Compare commits

...

5 Commits

121 changed files with 5396 additions and 2787 deletions

View File

@@ -672,17 +672,18 @@ model GalleryVideo {
// ========================================= LAYANAN DESA ========================================= // // ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan { model PelayananSuratKeterangan {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
deskripsi String @db.Text deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id]) image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String? imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id]) image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String? image2Id String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
AjukanPermohonan AjukanPermohonan[]
} }
model PelayananTelunjukSaktiDesa { model PelayananTelunjukSaktiDesa {
@@ -717,6 +718,20 @@ model PelayananPendudukNonPermanen {
isActive Boolean @default(true) isActive Boolean @default(true)
} }
model AjukanPermohonan {
id String @id @default(cuid())
nama String
nik String
alamat String
nomorKk String
kategori PelayananSuratKeterangan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENGHARGAAN ========================================= // // ========================================= PENGHARGAAN ========================================= //
model Penghargaan { model Penghargaan {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -835,8 +850,8 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id]) dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id]) pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
@@ -1254,15 +1269,15 @@ model KontakDaruratToItem {
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= // // ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas { model PencegahanKriminalitas {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String deskripsi String
deskripsiSingkat String deskripsiSingkat String
linkVideo String linkVideo String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
} }
// ========================================= LAPORAN PUBLIK ========================================= // // ========================================= LAPORAN PUBLIK ========================================= //

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -71,6 +71,22 @@ const pelayananPendudukNonPermanenForm = {
deskripsi: "", deskripsi: "",
}; };
const templateAjukanForm = z.object({
nama: z.string().min(1).max(5000),
nik: z.string().min(1).max(5000),
alamat: z.string().min(1).max(5000),
nomorKk: z.string().min(1).max(5000),
kategoriId: z.string().min(1).max(5000),
});
const defaultAjukanForm = {
nama: "",
nik: "",
alamat: "",
nomorKk: "",
kategoriId: "",
};
const suratKeterangan = proxy({ const suratKeterangan = proxy({
create: { create: {
form: { ...suratKeteranganForm }, form: { ...suratKeteranganForm },
@@ -146,6 +162,30 @@ const suratKeterangan = proxy({
} }
}, },
}, },
findManyAll: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
omit: { isActive: true };
}>[] | null,
loading: false,
load: async () => {
suratKeterangan.findManyAll.loading = true;
try {
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan["findManyAll"].get();
if (res.status === 200 && res.data?.success) {
suratKeterangan.findManyAll.data = res.data.data || [];
} else {
suratKeterangan.findManyAll.data = [];
console.error("Failed to load surat keterangan all:", res.data?.message);
}
} catch (error) {
console.error("Error loading surat keterangan all:", error);
suratKeterangan.findManyAll.data = [];
} finally {
suratKeterangan.findManyAll.loading = false;
}
},
},
findUnique: { findUnique: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{ data: null as Prisma.PelayananSuratKeteranganGetPayload<{
include: { include: {
@@ -769,11 +809,250 @@ const pelayananPendudukNonPermanen = proxy({
}, },
}); });
const ajukanPermohonan = proxy({
create: {
form: { ...defaultAjukanForm },
loading: false,
async create() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.create.loading = true;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"create"
].post(ajukanPermohonan.create.form);
if (res.status === 200) {
ajukanPermohonan.findMany.load();
return toast.success("Ajukan permohonan berhasil disimpan!");
}
return toast.error("Gagal menyimpan ajukan permohonan");
} catch (error) {
console.log((error as Error).message);
} finally {
ajukanPermohonan.create.loading = false;
}
},
resetForm() {
ajukanPermohonan.create.form = { ...defaultAjukanForm };
},
},
findMany: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
};
}>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
ajukanPermohonan.findMany.loading = true; // Use the full path to access the property
ajukanPermohonan.findMany.page = page;
ajukanPermohonan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"findMany"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
ajukanPermohonan.findMany.data = res.data.data || [];
ajukanPermohonan.findMany.total = res.data.total || 0;
ajukanPermohonan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load ajukan permohonan:", res.data?.message);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading ajukan permohonan:", error);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
} finally {
ajukanPermohonan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/desa/ajukanpermohonan/${id}`
);
if (res.ok) {
const data = await res.json();
ajukanPermohonan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch ajukan permohonan:", res.statusText);
ajukanPermohonan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
ajukanPermohonan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
ajukanPermohonan.delete.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan permohonan berhasil dihapus");
await ajukanPermohonan.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus ajukan permohonan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus ajukan permohonan");
} finally {
ajukanPermohonan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultAjukanForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/desa/ajukanpermohonan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
nik: data.nik,
alamat: data.alamat,
nomorKk: data.nomorKk,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.edit.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nik: this.form.nik,
alamat: this.form.alamat,
nomorKk: this.form.nomorKk,
kategoriId: this.form.kategoriId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Ajukan permohonan berhasil diupdate");
await ajukanPermohonan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate ajukan permohonan"
);
}
} catch (error) {
console.error("Error updating ajukan permohonan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update ajukan permohonan"
);
return false;
} finally {
ajukanPermohonan.edit.loading = false;
}
},
},
});
const stateLayananDesa = proxy({ const stateLayananDesa = proxy({
suratKeterangan, suratKeterangan,
pelayananPerizinanBerusaha, pelayananPerizinanBerusaha,
pelayananTelunjukSaktiDesa, pelayananTelunjukSaktiDesa,
pelayananPendudukNonPermanen, pelayananPendudukNonPermanen,
ajukanPermohonan,
}); });
export default stateLayananDesa; export default stateLayananDesa;

View File

@@ -26,14 +26,6 @@ const templateForm = z.object({
dokumenJadwalKegiatan: z.object({ dokumenJadwalKegiatan: z.object({
content: z.string().min(1, "Content minimal 1 karakter"), content: z.string().min(1, "Content minimal 1 karakter"),
}), }),
pendaftaranJadwalKegiatan: z.object({
name: z.string().min(1, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
}),
}); });
const defaultForm = { const defaultForm = {
@@ -55,15 +47,7 @@ const defaultForm = {
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: "", content: "",
}, }
pendaftaranJadwalKegiatan: {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
},
}; };
const jadwalkegiatanState = proxy({ const jadwalkegiatanState = proxy({
@@ -116,7 +100,6 @@ const jadwalkegiatanState = proxy({
deskripsijadwalkegiatan: true; deskripsijadwalkegiatan: true;
layananjadwalkegiatan: true; layananjadwalkegiatan: true;
dokumenjadwalkegiatan: true; dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
}; };
}>[] }>[]
| null, | null,
@@ -161,7 +144,6 @@ const jadwalkegiatanState = proxy({
layananjadwalkegiatan: true; layananjadwalkegiatan: true;
syaratketentuanjadwalkegiatan: true; syaratketentuanjadwalkegiatan: true;
dokumenjadwalkegiatan: true; dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
}; };
}> | null, }> | null,
loading: false, loading: false,
@@ -209,15 +191,7 @@ const jadwalkegiatanState = proxy({
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: data.dokumenjadwalkegiatan.content, content: data.dokumenjadwalkegiatan.content,
}, }
pendaftaranJadwalKegiatan: {
name: data.pendaftaranjadwalkegiatan.name,
tanggal: data.pendaftaranjadwalkegiatan.tanggal,
namaOrangtua: data.pendaftaranjadwalkegiatan.namaOrangtua,
nomor: data.pendaftaranjadwalkegiatan.nomor,
alamat: data.pendaftaranjadwalkegiatan.alamat,
catatan: data.pendaftaranjadwalkegiatan.catatan,
},
}; };
}, },
async submit() { async submit() {
@@ -259,20 +233,6 @@ const jadwalkegiatanState = proxy({
content: content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content, jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
}, },
pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
.namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
},
}; };
const res = await fetch( const res = await fetch(

View File

@@ -354,14 +354,39 @@ const kategoriKegiatan = proxy({
id: string; id: string;
nama: string; nama: string;
}> | null, }> | null,
async load() { page: 1,
const res = await ApiFetch.api.lingkungan.kategorikegiatan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
kategoriKegiatan.findMany.data = res.data?.data ?? []; kategoriKegiatan.findMany.loading = true; // ✅ Akses langsung via nama path
} kategoriKegiatan.findMany.page = page;
}, kategoriKegiatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriKegiatan.findMany.data = res.data.data ?? [];
kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori kegiatan paginated:", err);
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
} finally {
kategoriKegiatan.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.KategoriKegiatanGetPayload<{ data: null as Prisma.KategoriKegiatanGetPayload<{

View File

@@ -4,7 +4,7 @@ import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, 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'; import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -37,6 +37,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent", href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />, icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent" tooltip: "Pendataan penduduk non-permanent"
},
{
label: "Ajukan Permohonan",
value: "ajukanpermohonan",
href: "/admin/desa/layanan/ajukan_permohonan",
icon: <IconUsersPlus size={18} stroke={1.8} />,
tooltip: "Ajukan permohonan"
} }
]; ];

View File

@@ -5,7 +5,6 @@ import {
Button, Button,
Center, Center,
Group, Group,
Image,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -18,7 +17,7 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } 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';
@@ -87,7 +86,6 @@ function ListBerita({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh> <TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh> <TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -96,7 +94,7 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '30%' }}> <TableTd style={{ width: '30%' }}>
<Box w={200}> <Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.judul} {item.judul}
</Text> </Text>
@@ -107,19 +105,6 @@ function ListBerita({ search }: { search: string }) {
{item.kategoriBerita?.name || '-'} {item.kategoriBerita?.name || '-'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '25%' }}>
<Box
w={80}
h={80}
style={{ borderRadius: 8, overflow: 'hidden' }}
>
{item.image?.link ? (
<Image loading='lazy' src={item.image.link} alt="gambar" fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '15%' }}>
<Button <Button
variant="light" variant="light"

View File

@@ -0,0 +1,178 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Select,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditAjukanPermohonan() {
const router = useRouter();
const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
const [formData, setFormData] = useState({
nama: stateAjukan.edit.form.nama,
nik: stateAjukan.edit.form.nik,
alamat: stateAjukan.edit.form.alamat,
nomorKk: stateAjukan.edit.form.nomorKk,
kategoriId: stateAjukan.edit.form.kategoriId,
});
useEffect(() => {
stateLayananDesa.suratKeterangan.findManyAll.load();
const loadAjukan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateAjukan.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
nik: data.nik || '',
alamat: data.alamat || '',
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
}
} catch (error) {
console.error('Error loading ajukan:', error);
toast.error('Gagal memuat data ajukan');
}
};
loadAjukan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateAjukan.edit.form = {
...stateAjukan.edit.form,
...formData,
};
toast.success('Ajukan berhasil diperbarui!');
router.push('/admin/desa/layanan/ajukan_permohonan');
} catch (error) {
console.error('Error updating ajukan:', error);
toast.error('Terjadi kesalahan saat memperbarui ajukan');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<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">
Edit Ajukan Permohonan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
required
/>
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
value={formData.nik}
onChange={(e) => setFormData({ ...formData, nik: e.target.value })}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
required
/>
<TextInput
type="number"
label="Nomor KK"
placeholder="Masukkan nomor KK"
value={formData.nomorKk}
onChange={(e) => setFormData({ ...formData, nomorKk: e.target.value })}
required
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={formData.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateAjukan.edit.form.kategoriId = selected.id;
}
} else {
stateAjukan.edit.form.kategoriId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditAjukanPermohonan;

View File

@@ -0,0 +1,172 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailAjukanPermohonan() {
const ajukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
ajukanPermohonanState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
ajukanPermohonanState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/desa/layanan/ajukan_permohonan');
}
};
if (!ajukanPermohonanState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = ajukanPermohonanState.findUnique.data;
return (
<Box py={10}>
{/* Tombol Kembali */}
<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 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?.nama || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
NIK
</Text>
<Text fz="md" c="dimmed">
{data?.nik || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Alamat
</Text>
<Text fz="md" c="dimmed">
{data?.alamat || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Nomor KK
</Text>
<Text fz="md" c="dimmed">
{data?.nomorKk || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.kategori.name || '-'}
</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={ajukanPermohonanState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/desa/layanan/ajukan_permohonan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus ajukan permohonan ini?"
/>
</Box>
);
}
export default DetailAjukanPermohonan;

View File

@@ -0,0 +1,155 @@
/* 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,
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateLayananDesa from '../../../_state/desa/layananDesa';
function AjukanPermohonan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pelayanan Ajukan Permohonan'
placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAjukanPermohonan search={search} />
</Box>
);
}
function ListAjukanPermohonan({ search }: { search: string }) {
const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = AjukanPermohonanState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Title order={4}>List Ajukan Permohonan</Title>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Alamat</TableTh>
<TableTh style={{ width: '15%' }}>NIK</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.length > 0 ? (
data.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nik}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data ajukan permohonan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</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>
</Box>
);
}
export default AjukanPermohonan;

View File

@@ -94,9 +94,11 @@ function ListPengumuman({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.judul} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.judul}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">

View File

@@ -1,7 +1,7 @@
/* 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, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core'; import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash, IconPlus } 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';
@@ -60,7 +60,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
<Box py={10}> <Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Stack>
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}> <Group justify="space-between">
<Title order={4}>List Kategori Potensi</Title> <Title order={4}>List Kategori Potensi</Title>
<Tooltip label="Tambah Kategori Potensi" withArrow> <Tooltip label="Tambah Kategori Potensi" withArrow>
<Button <Button
@@ -72,7 +72,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip> </Tooltip>
</Box> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}> <Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>

View File

@@ -114,12 +114,22 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.name} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.dokterdantenagamedis?.name || '-'}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.tarifdanlayanan?.layanan || '-'}
</Box>
</TableTd> </TableTd>
<TableTd>{item.dokterdantenagamedis?.name || '-'}</TableTd>
<TableTd>{item.tarifdanlayanan?.layanan || '-'}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"

View File

@@ -149,16 +149,30 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd> <TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', { <Box w={150}>
day: '2-digit', {item.nama}
month: 'long', </Box>
year: 'numeric', </TableTd>
})} <TableTd>
<Box w={150}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.jenisKelamin}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.penyakit}
</Box>
</TableTd> </TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.penyakit}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"
@@ -212,24 +226,26 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? ( {mounted && diseaseChartData.length > 0 ? (
<BarChart <Center>
width={isMobile ? 450 : isTablet ? 500 : 550} <BarChart
height={350} width={isMobile ? 320 : isTablet ? 600 : 800} // kecilin biar muat
data={diseaseChartData} height={350}
> data={diseaseChartData}
<XAxis >
dataKey="name" <XAxis
tick={{ fontSize: 12 }} dataKey="name"
interval={0} tick={{ fontSize: 12 }}
angle={-45} interval={0}
textAnchor="end" angle={-45}
height={70} textAnchor="end"
/> height={70}
<YAxis /> />
<ChartTooltip /> <YAxis />
<Legend /> <ChartTooltip />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" /> <Legend />
</BarChart> <Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart>
</Center>
) : ( ) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text> <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)} )}

View File

@@ -41,14 +41,6 @@ interface JadwalKegiatanFormBase {
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: string; content: string;
}; };
pendaftaranJadwalKegiatan: {
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
};
} }
function EditJadwalKegiatan() { function EditJadwalKegiatan() {
@@ -76,14 +68,6 @@ function EditJadwalKegiatan() {
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: stateJadwalKegiatan.edit.form.dokumenJadwalKegiatan?.content || '', content: stateJadwalKegiatan.edit.form.dokumenJadwalKegiatan?.content || '',
}, },
pendaftaranJadwalKegiatan: {
name: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.name || '',
tanggal: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.tanggal || '',
namaOrangtua: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.namaOrangtua || '',
nomor: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.nomor || '',
alamat: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.alamat || '',
catatan: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.catatan || '',
},
}); });
useEffect(() => { useEffect(() => {
@@ -115,14 +99,6 @@ function EditJadwalKegiatan() {
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: form.dokumenJadwalKegiatan?.content || '', content: form.dokumenJadwalKegiatan?.content || '',
}, },
pendaftaranJadwalKegiatan: {
name: form.pendaftaranJadwalKegiatan?.name || '',
tanggal: form.pendaftaranJadwalKegiatan?.tanggal || '',
namaOrangtua: form.pendaftaranJadwalKegiatan?.namaOrangtua || '',
nomor: form.pendaftaranJadwalKegiatan?.nomor || '',
alamat: form.pendaftaranJadwalKegiatan?.alamat || '',
catatan: form.pendaftaranJadwalKegiatan?.catatan || '',
},
}); });
} }
} catch (error) { } catch (error) {
@@ -142,8 +118,7 @@ function EditJadwalKegiatan() {
deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan }, deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan },
layananJadwalKegiatan: { ...formData.layananJadwalKegiatan }, layananJadwalKegiatan: { ...formData.layananJadwalKegiatan },
syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan }, syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan },
dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan }, dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan }
pendaftaranJadwalKegiatan: { ...formData.pendaftaranJadwalKegiatan },
}; };
const success = await stateJadwalKegiatan.edit.submit(); const success = await stateJadwalKegiatan.edit.submit();
@@ -252,7 +227,7 @@ function EditJadwalKegiatan() {
{/* Dokumen */} {/* Dokumen */}
<Box> <Box>
<Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Dokumen Yang Perlu Dibawa</Text>
<EditEditor <EditEditor
value={formData.dokumenJadwalKegiatan.content} value={formData.dokumenJadwalKegiatan.content}
onChange={(val) => setFormData((prev) => ({ onChange={(val) => setFormData((prev) => ({
@@ -262,41 +237,6 @@ function EditJadwalKegiatan() {
/> />
</Box> </Box>
{/* Pendaftaran */}
<Box>
<Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text>
<TextInput label="Nama" value={formData.pendaftaranJadwalKegiatan.name}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, name: e.target.value }
}))}
/>
<TextInput type="date" label="Tanggal" value={formData.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, tanggal: e.target.value }
}))}
/>
<TextInput label="Nama Orangtua" value={formData.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, namaOrangtua: e.target.value }
}))}
/>
<TextInput label="Nomor" value={formData.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, nomor: e.target.value }
}))}
/>
<TextInput label="Alamat" value={formData.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, alamat: e.target.value }
}))}
/>
<TextInput label="Catatan" value={formData.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, catatan: e.target.value }
}))}
/>
</Box>
{/* Submit */} {/* Submit */}
<Group justify="right"> <Group justify="right">
<Button <Button

View File

@@ -109,18 +109,7 @@ function DetailJadwalKegiatan() {
<Text fz="lg" fw="bold">Dokumen</Text> <Text fz="lg" fw="bold">Dokumen</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.dokumenjadwalkegiatan.content }} /> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.dokumenjadwalkegiatan.content }} />
</Box> </Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fz="lg" fw="bold">Prosedur Pendaftaran</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.name}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.tanggal}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.namaOrangtua}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.nomor}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.alamat}</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.pendaftaranjadwalkegiatan.catatan }} />
</Box>
{/* Aksi */} {/* Aksi */}
<Group gap="sm"> <Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top"> <Tooltip label="Hapus Data" withArrow position="top">

View File

@@ -42,15 +42,7 @@ function CreateJadwalKegiatan() {
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: '', content: '',
}, }
pendaftaranJadwalKegiatan: {
name: '',
tanggal: '',
namaOrangtua: '',
nomor: '',
alamat: '',
catatan: '',
},
}; };
}; };
@@ -173,7 +165,7 @@ function CreateJadwalKegiatan() {
</Box> </Box>
<Box> <Box>
<Text fz="md" fw="bold" mb="sm">Dokumen</Text> <Text fz="md" fw="bold" mb="sm">Dokumen Yang Perlu Dibawa</Text>
<CreateEditor <CreateEditor
value={stateJadwalKegiatan.create.form.dokumenJadwalKegiatan.content} value={stateJadwalKegiatan.create.form.dokumenJadwalKegiatan.content}
onChange={(e) => { onChange={(e) => {
@@ -181,65 +173,6 @@ function CreateJadwalKegiatan() {
}} }}
/> />
</Box> </Box>
<Box>
<Text fz="md" fw="bold" mb="sm">Pendaftaran Jadwal Kegiatan</Text>
<TextInput
label="Nama"
required
placeholder="Masukkan nama"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name = e.target.value;
}}
/>
<TextInput
type="date"
required
label="Tanggal"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal = e.target.value;
}}
/>
<TextInput
label="Nama Orangtua"
required
placeholder="Masukkan nama orangtua"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua = e.target.value;
}}
/>
<TextInput
label="Nomor"
required
placeholder="Masukkan nomor"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor = e.target.value;
}}
/>
<TextInput
label="Alamat"
required
placeholder="Masukkan alamat"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat = e.target.value;
}}
/>
<TextInput
label="Catatan"
required
placeholder="Masukkan catatan"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan = e.target.value;
}}
/>
</Box>
{/* Save Button */} {/* Save Button */}
<Group justify="right"> <Group justify="right">
<Button <Button

View File

@@ -111,11 +111,14 @@ function ListJadwalKegiatan({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.informasijadwalkegiatan.name} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.informasijadwalkegiatan.name}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}>
{new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString(
'id-ID', 'id-ID',
{ {
@@ -124,12 +127,19 @@ function ListJadwalKegiatan({ search }: { search: string }) {
year: 'numeric', year: 'numeric',
} }
)} )}
</Box>
</TableTd> </TableTd>
<TableTd>{item.informasijadwalkegiatan.waktu}</TableTd>
<TableTd> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Box w={150}>
{item.informasijadwalkegiatan.lokasi} {item.informasijadwalkegiatan.waktu}
</Text> </Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed">
{item.informasijadwalkegiatan.lokasi}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -124,22 +124,32 @@ function ListKelahiran({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.nama} {item.nama}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}>
{new Date(item.tanggal).toLocaleDateString('id-ID', { {new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Box>
</TableTd> </TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd> <TableTd>
<Box w={150}>
{item.jenisKelamin}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed"> <Text truncate fz="sm" c="dimmed">
{item.alamat} {item.alamat}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -13,152 +13,150 @@ import colors from '@/con/colors';
function DetailKematian() { function DetailKematian() {
const state = useProxy(persentaseKelahiranKematian.kematian); const state = useProxy(persentaseKelahiranKematian.kematian);
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/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran"); router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran");
} }
}; };
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={500} radius="md" /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box py={10}>
{/* Tombol kembali */} {/* Tombol kembali */}
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />} leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15} mb={15}
> >
Kembali Kembali
</Button> </Button>
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "50%" }} w={{ base: "100%", md: "50%" }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
shadow="sm" shadow="sm"
> >
<Stack gap="md"> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Kematian Detail Data Kematian
</Text> </Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box> <Box>
<Text fz="lg" fw="bold">Nama</Text> <Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text> <Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz="lg" fw="bold">Tanggal</Text> <Text fz="lg" fw="bold">Tanggal</Text>
<Text fz="md" c="dimmed"> <Text fz="md" c="dimmed">
{data?.tanggal instanceof Date {new Date(data.tanggal).toLocaleDateString("id-ID", {
? data.tanggal.toLocaleDateString('id-ID', { day: "2-digit",
day: '2-digit', month: "long",
month: 'long', year: "numeric",
year: 'numeric' })}
}) </Text>
: data?.tanggal || '-'} </Box>
</Text>
</Box>
<Box> <Box>
<Text fz="lg" fw="bold">Jenis Kelamin</Text> <Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{data?.jenisKelamin || '-'}</Text> <Text fz="md" c="dimmed">{data?.jenisKelamin || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz="lg" fw="bold">Alamat</Text> <Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data?.alamat || '-'}</Text> <Text fz="md" c="dimmed">{data?.alamat || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz="lg" fw="bold">Penyebab</Text> <Text fz="lg" fw="bold">Penyebab</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.penyebab || '-' }} /> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.penyebab || '-' }} />
</Box> </Box>
<Group gap="sm"> <Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top"> <Tooltip label="Hapus Data" withArrow position="top">
<Button <Button
color="red" color="red"
onClick={() => { onClick={() => {
setSelectedId(data.id); setSelectedId(data.id);
setModalHapus(true); setModalHapus(true);
}} }}
variant="light" variant="light"
radius="md" radius="md"
size="md" size="md"
> >
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip label="Edit Data" withArrow position="top"> <Tooltip label="Edit Data" withArrow position="top">
<Button <Button
color="green" color="green"
onClick={() => router.push( onClick={() => router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${data.id}/edit` `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${data.id}/edit`
)} )}
variant="light" variant="light"
radius="md" radius="md"
size="md" size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Tooltip> </Tooltip>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus data ini?" text="Apakah Anda yakin ingin menghapus data ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -122,19 +122,33 @@ function ListKematian({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.nama} {item.nama}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}>
{new Date(item.tanggal).toLocaleDateString('id-ID', { {new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.jenisKelamin}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed">
{item.alamat}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd> <TableTd>
<Button <Button
variant="light" variant="light"

View File

@@ -4,7 +4,7 @@ import {
Box, Box,
Button, Button,
Center, Center,
Image, Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -17,16 +17,15 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
Group,
} from '@mantine/core'; } from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
function InfoWabahPenyakit() { function InfoWabahPenyakit() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -96,7 +95,6 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh>Judul</TableTh>
<TableTh>Deskripsi Singkat</TableTh> <TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -105,17 +103,18 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={200}>
{item.name} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.name}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate fz="sm" c="dimmed"> <Box w={200}>
{item.deskripsiSingkat} <Text truncate fz="sm" c="dimmed">
</Text> {item.deskripsiSingkat}
</TableTd> </Text>
<TableTd> </Box>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -4,7 +4,7 @@ import {
Box, Box,
Button, Button,
Center, Center,
Image, Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -17,7 +17,7 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -71,7 +71,7 @@ function ListKontakDarurat({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Stack mb="md" gap="sm"> <Stack mb="md" gap="sm">
<Box display="flex" style={{ justifyContent: "space-between", alignItems: "center" }}> <Group justify="space-between">
<Title order={4}>Daftar Kontak Darurat</Title> <Title order={4}>Daftar Kontak Darurat</Title>
<Tooltip label="Tambah Kontak Darurat" withArrow> <Tooltip label="Tambah Kontak Darurat" withArrow>
<Button <Button
@@ -83,7 +83,7 @@ function ListKontakDarurat({ search }: { search: string }) {
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip> </Tooltip>
</Box> </Group>
</Stack> </Stack>
{/* Tabel */} {/* Tabel */}
@@ -93,7 +93,6 @@ function ListKontakDarurat({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -102,15 +101,16 @@ function ListKontakDarurat({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.name} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.name}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Box w={200}>
</TableTd> <Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<TableTd> </Box>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -5,7 +5,6 @@ import {
Button, Button,
Center, Center,
Group, Group,
Image,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -18,15 +17,15 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function PenangananDarurat() { function PenangananDarurat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -91,7 +90,6 @@ function ListPenangananDarurat({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -100,21 +98,22 @@ function ListPenangananDarurat({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.name} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.name}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text <Box w={200}>
fz="sm" <Text
c="dimmed" fz="sm"
truncate c="dimmed"
lineClamp={1} truncate
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/> />
</TableTd> </Box>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" loading="lazy"/>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -107,21 +107,28 @@ function ListPosyandu({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '25%' }}> <TableTd style={{ width: '25%' }}>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd style={{ width: '20%' }}>
<Box w={150}>
<Text truncate fz="sm" c="dimmed"> <Text truncate fz="sm" c="dimmed">
{item.nomor || '-'} {item.nomor || '-'}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '30%' }}> <TableTd style={{ width: '30%' }}>
<Box w={150}>
<Text <Text
lineClamp={1}
truncate truncate
fz="sm" fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/> />
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '15%' }}>
<Button <Button

View File

@@ -5,7 +5,6 @@ import {
Button, Button,
Center, Center,
Group, Group,
Image,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -18,15 +17,15 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
function ProgramKesehatan() { function ProgramKesehatan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -90,7 +89,7 @@ function ListProgramKesehatan({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh>Judul</TableTh>
<TableTh>Deskripsi Singkat</TableTh> <TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -105,11 +104,13 @@ function ListProgramKesehatan({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Box w={200}>
<Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> <Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/> <Box w={200}>
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -78,8 +78,9 @@ function DetailPuskesmas() {
<Box> <Box>
<Text fz="lg" fw="bold">Jam Operasional</Text> <Text fz="lg" fw="bold">Jam Operasional</Text>
<Text fz="md" c="dimmed">{data?.jam?.workDays || '-'}</Text> <Text fz="md" c="dimmed">Senin - Jumat</Text>
<Text fz="md" c="dimmed">{data?.jam?.weekDays || '-'}</Text> <Text fz="md" c="dimmed">{data?.jam?.workDays || '-'} - {data?.jam?.weekDays || '-'}</Text>
<Text fz="md" c="dimmed">Sabtu - Minggu / Hari Libur</Text>
<Text fz="md" c="dimmed">{data?.jam?.holiday || '-'}</Text> <Text fz="md" c="dimmed">{data?.jam?.holiday || '-'}</Text>
</Box> </Box>
@@ -94,9 +95,13 @@ function DetailPuskesmas() {
<Box> <Box>
<Text fz="lg" fw="bold">Kontak</Text> <Text fz="lg" fw="bold">Kontak</Text>
<Text fz="md" c="dimmed">Kontak Puskesmas</Text>
<Text fz="md" c="dimmed">{data?.kontak?.kontakPuskesmas || '-'}</Text> <Text fz="md" c="dimmed">{data?.kontak?.kontakPuskesmas || '-'}</Text>
<Text fz="md" c="dimmed">Email</Text>
<Text fz="md" c="dimmed">{data?.kontak?.email || '-'}</Text> <Text fz="md" c="dimmed">{data?.kontak?.email || '-'}</Text>
<Text fz="md" c="dimmed">Facebook</Text>
<Text fz="md" c="dimmed">{data?.kontak?.facebook || '-'}</Text> <Text fz="md" c="dimmed">{data?.kontak?.facebook || '-'}</Text>
<Text fz="md" c="dimmed">Kontak UGD</Text>
<Text fz="md" c="dimmed">{data?.kontak?.kontakUGD || '-'}</Text> <Text fz="md" c="dimmed">{data?.kontak?.kontakUGD || '-'}</Text>
</Box> </Box>

View File

@@ -5,7 +5,6 @@ import {
Button, Button,
Center, Center,
Group, Group,
Image,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -18,7 +17,7 @@ import {
TableTr, TableTr,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -90,7 +89,7 @@ function ListPuskesmas({ search }: { search: string }) {
<TableTr> <TableTr>
<TableTh>Nama Puskesmas</TableTh> <TableTh>Nama Puskesmas</TableTh>
<TableTh>Alamat</TableTh> <TableTh>Alamat</TableTh>
<TableTh>Image</TableTh> <TableTh>Kontak</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -99,13 +98,25 @@ function ListPuskesmas({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={150}>
{item.name} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.name}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image.link} alt="image" radius="md" loading="lazy"/> <Box w={150}>
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.kontak.kontakPuskesmas}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -87,7 +87,9 @@ function ListAPBDes({ search }: { search: string }) {
<Text fw={500} truncate="end">{item.name}</Text> <Text fw={500} truncate="end">{item.name}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text>Rp. {item.jumlah}</Text> <Box w={150}>
<Text>Rp. {item.jumlah}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
{item.file?.link ? ( {item.file?.link ? (

View File

@@ -93,7 +93,9 @@ function ListKategoriKegiatan({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500}>{item.name}</Text> <Box w={200}>
<Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Tooltip label="Edit" withArrow> <Tooltip label="Edit" withArrow>

View File

@@ -98,9 +98,11 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '30%' }}> <TableTd style={{ width: '30%' }}>
<Text fz="sm" c="dimmed"> <Box w={200}>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.kategori?.name || '-'} {item.kategori?.name || '-'}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}> <TableTd style={{ width: '20%', textAlign: 'center' }}>
<Button <Button

View File

@@ -154,7 +154,7 @@ function Page() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={220} size={180}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
@@ -185,7 +185,7 @@ function Page() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={220} size={180}
data={donutDataRating} data={donutDataRating}
/> />
</Center> </Center>
@@ -216,7 +216,7 @@ function Page() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={220} size={180}
data={donutDataKelompokUmur} data={donutDataKelompokUmur}
/> />
</Center> </Center>

View File

@@ -88,66 +88,74 @@ function ListResponden({ search }: ListRespondenProps) {
<Title order={4} mb="sm"> <Title order={4} mb="sm">
Daftar Responden Daftar Responden
</Title> </Title>
<Table <Box style={{ overflowX: 'auto' }}>
striped <Table
highlightOnHover striped
withRowBorders highlightOnHover
verticalSpacing="sm" withRowBorders
> verticalSpacing="sm"
<TableThead> >
<TableTr> <TableThead>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '25%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr> <TableTr>
<TableTd colSpan={5}> <TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<Text ta="center" c="dimmed"> <TableTh style={{ width: '25%', textAlign: 'center' }}>Nama</TableTh>
Tidak ditemukan data dengan kata kunci pencarian <TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
</Text> <TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
</TableTd> <TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr> </TableTr>
) : ( </TableThead>
filteredData.map((item, index) => ( <TableTbody>
<TableTr key={item.id}> {filteredData.length === 0 ? (
<TableTd ta="center">{index + 1}</TableTd> <TableTr>
<TableTd ta="center">{item.name}</TableTd> <TableTd colSpan={5}>
<TableTd ta="center"> <Text ta="center" c="dimmed">
{item.tanggal Tidak ditemukan data dengan kata kunci pencarian
? new Date(item.tanggal).toLocaleDateString('id-ID', { </Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{index + 1}</TableTd>
<TableTd ta="center">{item.name}</TableTd>
<TableTd ta="center">
<Box w={150}>
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
}) })
: '-'} : '-'}
</TableTd> </Box>
<TableTd ta="center">{item.jenisKelamin.name}</TableTd> </TableTd>
<TableTd ta="center"> <TableTd ta="center">
<Button <Box w={100}>
size="xs" {item.jenisKelamin.name}
radius="md" </Box>
variant="light" </TableTd>
color="blue" <TableTd ta="center">
leftSection={<IconDeviceImac size={16} />} <Button
onClick={() => size="xs"
router.push( radius="md"
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}` variant="light"
) color="blue"
} leftSection={<IconDeviceImac size={16} />}
> onClick={() =>
Detail router.push(
</Button> `/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
</TableTd> )
</TableTr> }
)) >
)} Detail
</TableTbody> </Button>
</Table> </TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination

View File

@@ -96,7 +96,11 @@ function ListKategoriPrestasi({ search }: { search: string }) {
) : ( ) : (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd> <TableTd>
<Box w={200}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center', width: '120px' }}> <TableTd style={{ textAlign: 'center', width: '120px' }}>
<Tooltip label="Edit" withArrow position="top"> <Tooltip label="Edit" withArrow position="top">
<Button <Button

View File

@@ -78,9 +78,9 @@ function DetailPrestasiDesa() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Box <Box
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi || '-' }} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi || '-' }}
/> />
</Box> </Box>
@@ -91,10 +91,11 @@ function DetailPrestasiDesa() {
<Image <Image
src={detailState.findUnique.data.image.link} src={detailState.findUnique.data.image.link}
alt={detailState.findUnique.data.name || 'Gambar Prestasi'} alt={detailState.findUnique.data.name || 'Gambar Prestasi'}
w={300} w="100%"
maw={300} // max width 300px
fit="contain" fit="contain"
style={{ borderRadius: '8px', border: '1px solid #e0e0e0' }} style={{ borderRadius: '8px', border: '1px solid #e0e0e0' }}
loading='lazy' loading="lazy"
/> />
) : ( ) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text> <Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -104,7 +104,9 @@ function ListPrestasi({ search }: { search: string }) {
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd style={{ width: '25%' }}> <TableTd style={{ width: '25%' }}>
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text> <Box w={150}>
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '25%', textAlign: 'center' }}> <TableTd style={{ width: '25%', textAlign: 'center' }}>
<Tooltip label="Kelola Prestasi" withArrow> <Tooltip label="Kelola Prestasi" withArrow>

View File

@@ -51,8 +51,10 @@ function DetailMediaSosial() {
</Button> </Button>
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "60%" }} w="100%"
maw={500} // <= tambahkan ini, biar tidak lebih dari 500px
mx="auto" // center di layar
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -70,9 +72,9 @@ function DetailMediaSosial() {
<Text fz="md" c="dimmed">{data.name || '-'}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box >
<Text fz="lg" fw="bold">Link / Nomor Telepon</Text> <Text fz="lg" fw="bold">Link / Nomor Telepon</Text>
<Text fz="md" c="dimmed">{data.iconUrl || '-'}</Text> <Text fz="md" c="dimmed" style={{ wordBreak: "break-all", whiteSpace: "pre-wrap" }}>{data.iconUrl || '-'}</Text>
</Box> </Box>
<Box> <Box>
@@ -81,12 +83,14 @@ function DetailMediaSosial() {
<Image <Image
src={data.image.link} src={data.image.link}
alt={data.name || 'Gambar Media Sosial'} alt={data.name || 'Gambar Media Sosial'}
w={120} w="100%"
h={120} maw={120} // max width biar tidak keluar layar
h="auto"
radius="md" radius="md"
fit="cover" fit="cover"
loading="lazy" loading="lazy"
/> />
) : ( ) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text> <Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)} )}

View File

@@ -224,7 +224,7 @@ function EditPejabatDesa() {
alt="Preview" alt="Preview"
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
maxHeight: '200px', maxHeight: '150px',
objectFit: 'contain', objectFit: 'contain',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #ddd', border: '1px solid #ddd',

View File

@@ -72,7 +72,7 @@ function Page() {
<Image <Image
pt={{ base: 0, md: 60 }} pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"} src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }} w={{ base: 150, md: 350 }}
alt="Foto Profil Pejabat" alt="Foto Profil Pejabat"
radius="md" radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }} onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
@@ -87,7 +87,7 @@ function Page() {
className="glass3" className="glass3"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }} 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: "1rem", md: "1.6rem" }}>
{item.name} {item.name}
</Text> </Text>
</Paper> </Paper>

View File

@@ -1,6 +1,6 @@
'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, Title, Tooltip } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -49,9 +49,7 @@ function ListProgramInovasi({ search }: { search: string }) {
return ( return (
<Box py={15}> <Box py={15}>
<Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm"> <Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
<Box mb="md" display="flex" <Group justify='space-between'>
style={{ justifyContent: 'space-between', alignItems: 'center' }}
>
<Title order={4}>Daftar Program Inovasi</Title> <Title order={4}>Daftar Program Inovasi</Title>
<Tooltip label="Tambah Program Inovasi" withArrow> <Tooltip label="Tambah Program Inovasi" withArrow>
<Button <Button
@@ -64,7 +62,7 @@ function ListProgramInovasi({ search }: { search: string }) {
Tambah Program Tambah Program
</Button> </Button>
</Tooltip> </Tooltip>
</Box> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm"> <Table highlightOnHover striped verticalSpacing="sm">
<TableThead> <TableThead>

View File

@@ -3,7 +3,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import dataLingkunganDesaState from '@/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa'; import dataLingkunganDesaState from '@/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa';
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';
@@ -19,34 +29,34 @@ interface FormDataLingkunganDesa {
} }
type IconKey = type IconKey =
'ekowisata' | | 'ekowisata'
'kompetisi' | | 'kompetisi'
'wisata' | | 'wisata'
'ekonomi' | | 'ekonomi'
'sampah' | | 'sampah'
'truck' | | 'truck'
'scale' | | 'scale'
'clipboard' | | 'clipboard'
'trash' | | 'trash'
'lingkunganSehat' | | 'lingkunganSehat'
'sumberOksigen' | | 'sumberOksigen'
'ekonomiBerkelanjutan' | | 'ekonomiBerkelanjutan'
'mencegahBencana' | | 'mencegahBencana'
'rumah' | | 'rumah'
'pohon' | | 'pohon'
'air'; | 'air';
function EditDataLingkunganDesa() { function EditDataLingkunganDesa() {
const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState) const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState);
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const [formData, setFormData] = useState<FormDataLingkunganDesa>({ const [formData, setFormData] = useState<FormDataLingkunganDesa>({
name: '', name: '',
deskripsi: '', deskripsi: '',
jumlah: '', jumlah: '',
icon: '', icon: '',
}) });
useEffect(() => { useEffect(() => {
const loadProgramKreatif = async () => { const loadProgramKreatif = async () => {
@@ -56,7 +66,6 @@ function EditDataLingkunganDesa() {
try { try {
const data = await stateDataLingkunganDesa.update.load(id); const data = await stateDataLingkunganDesa.update.load(id);
if (data) { if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateDataLingkunganDesa.update.id = id; stateDataLingkunganDesa.update.id = id;
stateDataLingkunganDesa.update.form = { stateDataLingkunganDesa.update.form = {
@@ -74,16 +83,14 @@ function EditDataLingkunganDesa() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading data lingkungan desa:", error); console.error('Error loading data lingkungan desa:', error);
toast.error("Gagal memuat data data lingkungan desa"); toast.error('Gagal memuat data lingkungan desa');
} }
} };
loadProgramKreatif(); loadProgramKreatif();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateDataLingkunganDesa.update.form = { stateDataLingkunganDesa.update.form = {
@@ -92,49 +99,68 @@ function EditDataLingkunganDesa() {
deskripsi: formData.deskripsi.trim(), deskripsi: formData.deskripsi.trim(),
jumlah: formData.jumlah.trim(), jumlah: formData.jumlah.trim(),
icon: formData.icon.trim(), icon: formData.icon.trim(),
} };
await stateDataLingkunganDesa.update.submit(); await stateDataLingkunganDesa.update.submit();
router.push("/admin/lingkungan/data-lingkungan-desa"); toast.success('Data lingkungan desa berhasil diperbarui!');
router.push('/admin/lingkungan/data-lingkungan-desa');
} catch (error) { } catch (error) {
console.error("Error updating data lingkungan desa:", error); console.error('Error updating data lingkungan desa:', error);
toast.error("Gagal memuat data data lingkungan desa"); toast.error('Terjadi kesalahan saat memperbarui data lingkungan desa');
} }
} };
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={3}>Edit Data Lingkungan Desa</Title> <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">
Edit Data Lingkungan Desa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={formData.name} value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Data Lingkungan Desa</Text>} label={<Text fz="sm" fw="bold">Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa" placeholder="Masukkan nama data lingkungan desa"
onChange={(val) => { onChange={(val) =>
setFormData({ setFormData({
...formData, ...formData,
name: val.target.value name: val.target.value,
}) })
}} }
required
/> />
<TextInput <TextInput
value={formData.jumlah} value={formData.jumlah}
label={<Text fz={"sm"} fw={"bold"}>Jumlah Data Lingkungan Desa</Text>} label={<Text fz="sm" fw="bold">Jumlah Data Lingkungan Desa</Text>}
placeholder="masukkan jumlah data lingkungan desa" placeholder="Masukkan jumlah data lingkungan desa"
onChange={(val) => { onChange={(val) =>
setFormData({ setFormData({
...formData, ...formData,
jumlah: val.target.value jumlah: val.target.value,
}) })
}} }
required
/> />
<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) => {
@@ -143,16 +169,34 @@ function EditDataLingkunganDesa() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Data Lingkungan Desa</Text> <Text fz="sm" fw="bold" mb={6}>
Ikon Data Lingkungan Desa
</Text>
<SelectIconProgramEdit <SelectIconProgramEdit
value={formData.icon as IconKey} value={formData.icon as IconKey}
onChange={(value) => { onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value })); setFormData((prev) => ({ ...prev, icon: value }));
stateDataLingkunganDesa.update.form.icon = value; stateDataLingkunganDesa.update.form.icon = value;
}} /> }}
/>
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Group justify="flex-end">
<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>

View File

@@ -1,23 +1,40 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { 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, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconDroplet, IconEdit, IconHome, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTree, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react'; import {
IconArrowBack,
IconChartLine,
IconChristmasTreeFilled,
IconClipboard,
IconDroplet,
IconEdit,
IconHome,
IconHomeEco,
IconLeaf,
IconRecycle,
IconScale,
IconShieldFilled,
IconTent,
IconTrash,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruck,
} from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailDataLingkunganDesa() { function DetailDataLingkunganDesa() {
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const stateDataLingkungan = useProxy(dataLingkunganDesaState) const stateDataLingkungan = useProxy(dataLingkunganDesaState);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,
@@ -35,90 +52,117 @@ function DetailDataLingkunganDesa() {
mencegahBencana: IconShieldFilled, mencegahBencana: IconShieldFilled,
rumah: IconHome, rumah: IconHome,
pohon: IconTree, pohon: IconTree,
air: IconDroplet air: IconDroplet,
}; };
useShallowEffect(() => { useShallowEffect(() => {
stateDataLingkungan.findUnique.load(params?.id as string) stateDataLingkungan.findUnique.load(params?.id as string);
}, [params?.id]) }, [params?.id]);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateDataLingkungan.delete.byId(selectedId) stateDataLingkungan.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/lingkungan/data-lingkungan-desa") router.push('/admin/lingkungan/data-lingkungan-desa');
} }
} };
if (!stateDataLingkungan.findUnique.data) { if (!stateDataLingkungan.findUnique.data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( const data = stateDataLingkungan.findUnique.data;
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Lingkungan Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}> return (
<Stack gap={"xs"}> <Box py={10}>
{/* Back Button */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Main Card */}
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Title */}
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Lingkungan Desa
</Text>
{/* Content Card */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Data Lingkungan Desa</Text> <Text fz="lg" fw="bold">Nama Data Lingkungan Desa</Text>
<Text fz={"lg"}>{stateDataLingkungan.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Jumlah Data Lingkungan Desa</Text> <Text fz="lg" fw="bold">Jumlah Data Lingkungan Desa</Text>
<Text fz={"lg"}>{stateDataLingkungan.findUnique.data?.jumlah}</Text> <Text fz="md" c="dimmed">{data?.jumlah || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Ikon Data Lingkungan Desa</Text> <Text fz="lg" fw="bold">Ikon Data Lingkungan Desa</Text>
{iconMap[stateDataLingkungan.findUnique.data?.icon] && ( {iconMap[data?.icon] ? (
<Box title={stateDataLingkungan.findUnique.data?.icon}> <Box title={data?.icon}>
{React.createElement(iconMap[stateDataLingkungan.findUnique.data?.icon], { size: 24 })} {React.createElement(iconMap[data.icon], { size: 28, color: colors['blue-button'] })}
</Box> </Box>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)} )}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateDataLingkungan.findUnique.data?.deskripsi }}></Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box> </Box>
<Box>
<Flex gap={"xs"} mt={10}> {/* Action Buttons */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Data Lingkungan Desa" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateDataLingkungan.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateDataLingkungan.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={stateDataLingkungan.delete.loading || !stateDataLingkungan.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Data Lingkungan Desa" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateDataLingkungan.findUnique.data) { onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${data.id}/edit`)}
router.push(`/admin/lingkungan/data-lingkungan-desa/${stateDataLingkungan.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateDataLingkungan.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -130,7 +174,7 @@ function DetailDataLingkunganDesa() {
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data lingkungan desa ini?" text="Apakah anda yakin ingin menghapus data lingkungan desa ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,16 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
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';
@@ -8,60 +18,105 @@ import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon'; import SelectIconProgram from '../../../_com/selectIcon';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa'; import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
function CreateDataLingkunganDesa() { function CreateDataLingkunganDesa() {
const stateCreate = useProxy(dataLingkunganDesaState) const stateCreate = useProxy(dataLingkunganDesaState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateCreate.create.form = { stateCreate.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
jumlah: "", jumlah: '',
icon: "", icon: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/lingkungan/data-lingkungan-desa") router.push('/admin/lingkungan/data-lingkungan-desa');
} };
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={3}>Create Data Lingkungan Desa</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={22} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Lingkungan Desa
</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">
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Data Lingkungan Desa</Text>} label={<Text fw="bold" fz="sm">Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa" placeholder="Masukkan nama data lingkungan desa"
onChange={(val) => stateCreate.create.form.name = val.target.value} value={stateCreate.create.form.name || ''}
onChange={(val) => (stateCreate.create.form.name = val.target.value)}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Data Lingkungan Desa</Text> <Text fz="sm" fw="bold" mb={6}>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} /> Ikon Data Lingkungan Desa
</Box> </Text>
<TextInput <SelectIconProgram
type='number' onChange={(value) => (stateCreate.create.form.icon = value)}
onChange={(e) => stateCreate.create.form.jumlah = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Jmlah data lingkungan desa</Text>}
placeholder='Masukkan jumlah data lingkungan desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi data lingkungan desa</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/> />
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <TextInput
label={<Text fw="bold" fz="sm">Jumlah Data Lingkungan Desa</Text>}
placeholder="Masukkan jumlah data lingkungan desa"
value={stateCreate.create.form.jumlah || ''}
onChange={(e) => (stateCreate.create.form.jumlah = e.currentTarget.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Data Lingkungan Desa
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
{/* Submit Button */}
<Group justify="right" mt="sm">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,21 +2,23 @@
/* 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 {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconLeaf, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled,
IconDeviceImacCog, IconDroplet, IconHome, IconHomeEco, IconLeaf,
IconPlus,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent, IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent,
IconTrashFilled, IconTrashFilled, IconTree, IconTrendingUp, IconTrophy, IconTruckFilled
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { 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 dataLingkunganDesaState from '../../_state/lingkungan/data-lingkungan-desa'; import dataLingkunganDesaState from '../../_state/lingkungan/data-lingkungan-desa';
@@ -26,7 +28,7 @@ function DataLingkunganDesa() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Data Lingkungan Desa' title='Data Lingkungan Desa'
placeholder='pencarian' placeholder='Cari data lingkungan...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -64,76 +66,98 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
rumah: IconHome, rumah: IconHome,
pohon: IconTree, pohon: IconTree,
air: IconDroplet air: IconDroplet
}; };
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={650} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper p="md" > <Paper withBorder shadow="md" radius="md" p="lg" bg={colors['white-1']}>
<Stack> <Stack>
<JudulList <Group justify="space-between" mb="md">
title='List Data Lingkungan Desa' <Title order={4}>Daftar Data Lingkungan Desa</Title>
href='/admin/lingkungan/data-lingkungan-desa/create' <Tooltip label="Tambah Data Lingkungan Desa" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}>
<Table striped withTableBorder withRowBorders> Tambah Baru
</Button>
</Tooltip>
</Group>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh>No</TableTh>
<TableTh>Nama Data Lingkungan Desa</TableTh> <TableTh>Nama Data Lingkungan Desa</TableTh>
<TableTh>Jumlah Data Lingkungan Desa</TableTh> <TableTh>Jumlah</TableTh>
<TableTh>Ikon</TableTh> <TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
</Table> </Table>
<Text ta="center">Tidak ada data lingkungan desa yang tersedia</Text> <Center py={20}>
<Text c="dimmed">Tidak ada data lingkungan desa yang tersedia</Text>
</Center>
</Stack> </Stack>
</Paper> </Paper>
</Box > </Box >
); );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}> <Paper withBorder shadow="md" radius="md" bg={colors['white-1']} p="lg">
<JudulList <Group justify="space-between" mb="md">
title='List Data Lingkungan Desa' <Title order={4}>Daftar Data Lingkungan Desa</Title>
href='/admin/lingkungan/data-lingkungan-desa/create' <Tooltip label="Tambah Data Lingkungan Desa" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan/create')}>
<Box style={{ overflowY: 'auto' }}> Tambah Baru
<Table striped withTableBorder withRowBorders> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> <TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Data Lingkungan Desa</TableTh> <TableTh style={{ width: '25%' }}>Nama Data</TableTh>
<TableTh style={{ width: '35%' }}>Jumlah Data Lingkungan Desa</TableTh> <TableTh style={{ width: '35%' }}>Jumlah</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh> <TableTh style={{ width: '15%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh> <TableTh style={{ width: '20%', textAlign: 'center' }}>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd> <TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>± {item.jumlah}</TableTd> <Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
<TableTd style={{ width: '10%' }}> </TableTd>
<TableTd>
<Text fz="sm" c="dimmed">± {item.jumlah}</Text>
</TableTd>
<TableTd>
{iconMap[item.icon] && ( {iconMap[item.icon] && (
<Box title={item.icon}> <Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })} {React.createElement(iconMap[item.icon], { size: 22 })}
</Box> </Box>
)} )}
</TableTd> </TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}> <TableTd style={{ textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${item.id}`)}> <Button
<IconDeviceImac size={25} /> size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${item.id}`)}
>
Detail
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -147,11 +171,13 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,67 +1,116 @@
/* 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 { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBook, IconLeaf, IconSchool } 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';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const tabs = [ const tabs = [
{ {
label: "Tujuan Edukasi Lingkungan", label: "Tujuan Edukasi Lingkungan",
value: "tujuanedukasilingkungan", value: "tujuanedukasilingkungan",
href: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan" href: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan",
tooltip: "Lihat tujuan edukasi lingkungan",
icon: <IconLeaf size={18} stroke={1.8} />
}, },
{ {
label: "Materi Edukasi Yang Diberikan", label: "Materi Edukasi Yang Diberikan",
value: "materiedukasiyangdiberikan", value: "materiedukasiyangdiberikan",
href: "/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan" href: "/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan",
tooltip: "Kelola materi edukasi yang diberikan",
icon: <IconBook size={18} stroke={1.8} />
}, },
{ {
label: "Contoh Kegiatan Di Desa Darmasaba", label: "Contoh Kegiatan Di Desa Darmasaba",
value: "contohkegiatan", value: "contohkegiatan",
href: "/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba" href: "/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba",
tooltip: "Lihat contoh kegiatan desa Darmasaba",
icon: <IconSchool size={18} stroke={1.8} />
}, },
]; ];
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="md">
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Edukasi Lingkungan
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> <Tabs
))} color={colors['blue-button']}
</TabsList> variant="pills"
{tabs.map((e, i) => ( value={activeTab}
<TabsPanel key={i} value={e.value}> onChange={handleTabChange}
{/* Konten dummy, bisa diganti tergantung routing */} radius="lg"
<></> keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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",
whiteSpace: "nowrap",
transition: "all 0.2s ease",
}}
>
<span style={{
display: "inline-block",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}>
{tab.label}
</span>
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel key={i} value={tab.value}>
<Box p="md">
{children}
</Box>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); )
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -9,77 +9,105 @@ 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';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), { const EdukasiLingkunganTextEditor = dynamic(
ssr: false, () => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
}); { ssr: false }
);
function EditContohKegiatanDesaDarmasaba() { function EditContohKegiatanDesaDarmasaba() {
const router = useRouter() const router = useRouter();
const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan) const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan);
const [judul, setJudul] = useState(''); const [judul, setJudul] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useShallowEffect(() => { useShallowEffect(() => {
if (!contohEdukasiState.findById.data) { if (!contohEdukasiState.findById.data) {
contohEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu contohEdukasiState.findById.initialize();
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (contohEdukasiState.findById.data) { if (contohEdukasiState.findById.data) {
setJudul(contohEdukasiState.findById.data.judul ?? '') setJudul(contohEdukasiState.findById.data.judul ?? '');
setContent(contohEdukasiState.findById.data.deskripsi ?? '') setContent(contohEdukasiState.findById.data.deskripsi ?? '');
} }
}, [contohEdukasiState.findById.data]) }, [contohEdukasiState.findById.data]);
const submit = () => { const submit = () => {
if (contohEdukasiState.findById.data) { if (contohEdukasiState.findById.data) {
contohEdukasiState.findById.data.judul = judul; contohEdukasiState.findById.data.judul = judul;
contohEdukasiState.findById.data.deskripsi = content; contohEdukasiState.findById.data.deskripsi = content;
contohEdukasiState.update.save(contohEdukasiState.findById.data) contohEdukasiState.update.save(contohEdukasiState.findById.data);
} }
router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba') router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap={'xs'}> {/* Header */}
<Box> <Group mb="md">
<Button <Button
variant={'subtle'} variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
> p="xs"
<IconArrowBack color={colors['blue-button']} size={20} /> radius="md"
</Button> >
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Box> </Button>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> <Title order={4} ml="sm" c="dark">
<Stack gap={'xs'}> Edit Contoh Kegiatan di Desa Darmasaba
<Title order={3}>Edit Contoh Kegiatan Di Desa Darmasaba</Title> </Title>
<Text fw={"bold"}>Judul</Text> </Group>
<EdukasiLingkunganTextEditor
showSubmit={false} {/* Form Paper */}
onChange={setJudul} <Paper
initialContent={judul} bg={colors['white-1']}
/> p="lg"
<Text fw={"bold"}>Deskripsi</Text> radius="md"
<EdukasiLingkunganTextEditor shadow="sm"
showSubmit={false} w={{ base: '100%', md: '50%' }}
onChange={setContent} style={{ border: '1px solid #e0e0e0' }}
initialContent={content} >
/> <Stack gap="md">
<Group> <Box>
<Button <Text fw="bold" mb={6}>
bg={colors['blue-button']} Judul
onClick={submit} </Text>
loading={contohEdukasiState.update.loading} <EdukasiLingkunganTextEditor
> showSubmit={false}
Submit onChange={setJudul}
</Button> initialContent={judul}
</Group> />
</Stack> </Box>
</Paper>
</Box> <Box>
</Stack> <Text fw="bold" mb={6}>
Deskripsi
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={contohEdukasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } 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';
@@ -8,47 +7,72 @@ import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan'; import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listContohEdukasi = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan) const listContohEdukasi = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan);
useShallowEffect(() => { useShallowEffect(() => {
listContohEdukasi.findById.load('edit') listContohEdukasi.findById.load('edit');
}, []) }, []);
if (!listContohEdukasi.findById.data) { if (!listContohEdukasi.findById.data) {
return ( return (
<Stack> <Stack py={20}>
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={600} />
</Stack> </Stack>
) );
} }
return ( return (
<Paper bg={colors['white-1']} p={'md'} radius={10}> <Box p="md">
<Stack gap={"22"}> <Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid> <Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Contoh Kegiatan Di Desa Darmasaba</Title> <Title order={3} fw={600}>
Preview Contoh Kegiatan Di Desa Darmasaba
</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit')}> <Button
<IconEdit size={16} /> size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
'/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit'
)
}
>
Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
<Box>
<Stack gap={'lg'}> <Stack gap="md">
<Paper p={"xl"} bg={colors['BG-trans']}> <Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box px={{ base: 0, md: 30 }}> <Box mb="md">
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.judul }} /> <Text
</Box> fz={{ base: 'xl', md: '2xl' }}
<Box px={{ base: 0, md: 30 }}> fw={600}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.deskripsi }} /> c="black"
</Box> dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.judul }}
</Paper> />
</Stack> </Box>
</Box> <Box>
</Stack> <Text
</Paper> fz={{ base: 'md', md: 'lg' }}
) ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -9,77 +9,90 @@ 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';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), { const EdukasiLingkunganTextEditor = dynamic(
ssr: false, () => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
}); { ssr: false }
);
function EditMateriEdukasiYangDiberikan() { function EditMateriEdukasiYangDiberikan() {
const router = useRouter() const router = useRouter();
const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan) const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan);
const [judul, setJudul] = useState(''); const [judul, setJudul] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useShallowEffect(() => { useShallowEffect(() => {
if (!materiEdukasiState.findById.data) { if (!materiEdukasiState.findById.data) {
materiEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu materiEdukasiState.findById.initialize();
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (materiEdukasiState.findById.data) { if (materiEdukasiState.findById.data) {
setJudul(materiEdukasiState.findById.data.judul ?? '') setJudul(materiEdukasiState.findById.data.judul ?? '');
setContent(materiEdukasiState.findById.data.deskripsi ?? '') setContent(materiEdukasiState.findById.data.deskripsi ?? '');
} }
}, [materiEdukasiState.findById.data]) }, [materiEdukasiState.findById.data]);
const submit = () => { const submit = () => {
if (materiEdukasiState.findById.data) { if (materiEdukasiState.findById.data) {
materiEdukasiState.findById.data.judul = judul; materiEdukasiState.findById.data.judul = judul;
materiEdukasiState.findById.data.deskripsi = content; materiEdukasiState.findById.data.deskripsi = content;
materiEdukasiState.update.save(materiEdukasiState.findById.data) materiEdukasiState.update.save(materiEdukasiState.findById.data);
} }
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan') router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap={'xs'}> <Group mb="md">
<Box> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button <IconArrowBack color={colors['blue-button']} size={24} />
variant={'subtle'} </Button>
onClick={() => router.back()} <Title order={4} ml="sm" c="dark">
> Edit Materi Edukasi Yang Diberikan
<IconArrowBack color={colors['blue-button']} size={20} /> </Title>
</Button> </Group>
</Box>
<Box> <Paper
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> bg={colors['white-1']}
<Stack gap={'xs'}> p="lg"
<Title order={3}>Edit Materi Edukasi Yang Diberikan</Title> radius="md"
<Text fw={"bold"}>Judul</Text> shadow="sm"
<EdukasiLingkunganTextEditor w={{ base: '100%', md: '50%' }}
showSubmit={false} style={{ border: '1px solid #e0e0e0' }}
onChange={setJudul} >
initialContent={judul} <Stack gap="md">
/> <Box>
<Text fw={"bold"}>Content</Text> <Text fw="bold" mb={6}>
<EdukasiLingkunganTextEditor Judul
showSubmit={false} </Text>
onChange={setContent} <EdukasiLingkunganTextEditor showSubmit={false} onChange={setJudul} initialContent={judul} />
initialContent={content} </Box>
/>
<Group> <Box>
<Button <Text fw="bold" mb={6}>
bg={colors['blue-button']} Konten
onClick={submit} </Text>
loading={materiEdukasiState.update.loading} <EdukasiLingkunganTextEditor showSubmit={false} onChange={setContent} initialContent={content} />
> </Box>
Submit
</Button> <Group justify="right" mt="md">
</Group> <Button
</Stack> onClick={submit}
</Paper> radius="md"
</Box> size="md"
</Stack> style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
loading={materiEdukasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } 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';
@@ -10,44 +9,65 @@ import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkunga
function Page() { function Page() {
const router = useRouter() const router = useRouter()
const listMateriEdukasi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan) const listMateriEdukasi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan)
useShallowEffect(() => { useShallowEffect(() => {
listMateriEdukasi.findById.load('edit') listMateriEdukasi.findById.load('edit')
}, []) }, [])
if (!listMateriEdukasi.findById.data) { if (!listMateriEdukasi.findById.data) {
return ( return (
<Stack> <Stack py={20}>
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={600} />
</Stack> </Stack>
) )
} }
return ( return (
<Paper bg={colors['white-1']} p={'md'} radius={10}> <Box p="md">
<Stack gap={"22"}> <Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid> <Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Materi Edukasi Yang Diberikan</Title> <Title order={3} fw={600}>Preview Materi Edukasi Yang Diberikan</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit')}> <Button
<IconEdit size={16} /> size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit')
}
>
Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
<Box>
<Stack gap={'lg'}> <Stack gap="md">
<Paper p={"xl"} bg={colors['BG-trans']}> <Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box px={{ base: 0, md: 30 }}> <Box mb="md">
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.judul }} /> <Text
</Box> fz={{ base: 'xl', md: '2xl' }}
<Box px={{ base: 0, md: 30 }}> fw={600}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.deskripsi }} /> c="black"
</Box> dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.judul }}
</Paper> />
</Stack> </Box>
</Box> <Box>
</Stack> <Text
</Paper> fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
) )
} }

View File

@@ -9,77 +9,103 @@ 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';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), { const EdukasiLingkunganTextEditor = dynamic(
ssr: false, () => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
}); { ssr: false }
);
function EditTujuanEdukasiLingkungan() { function EditTujuanEdukasiLingkungan() {
const router = useRouter() const router = useRouter();
const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi) const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi);
const [judul, setJudul] = useState(''); const [judul, setJudul] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useShallowEffect(() => { useShallowEffect(() => {
if (!tujuanEdukasiState.findById.data) { if (!tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu tujuanEdukasiState.findById.initialize();
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (tujuanEdukasiState.findById.data) { if (tujuanEdukasiState.findById.data) {
setJudul(tujuanEdukasiState.findById.data.judul ?? '') setJudul(tujuanEdukasiState.findById.data.judul ?? '');
setContent(tujuanEdukasiState.findById.data.deskripsi ?? '') setContent(tujuanEdukasiState.findById.data.deskripsi ?? '');
} }
}, [tujuanEdukasiState.findById.data]) }, [tujuanEdukasiState.findById.data]);
const submit = () => { const submit = () => {
if (tujuanEdukasiState.findById.data) { if (tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.data.judul = judul; tujuanEdukasiState.findById.data.judul = judul;
tujuanEdukasiState.findById.data.deskripsi = content; tujuanEdukasiState.findById.data.deskripsi = content;
tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data) tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data);
} }
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan') router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap={'xs'}> <Group mb="md">
<Box> <Button
<Button variant="subtle"
variant={'subtle'} onClick={() => router.back()}
onClick={() => router.back()} p="xs"
> radius="md"
<IconArrowBack color={colors['blue-button']} size={20} /> >
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Box> <Title order={4} ml="sm" c="dark">
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> Edit Tujuan Edukasi Lingkungan
<Stack gap={'xs'}> </Title>
<Title order={3}>Edit Tujuan Edukasi Lingkungan</Title> </Group>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor <Paper
showSubmit={false} bg={colors['white-1']}
onChange={setJudul} p="lg"
initialContent={judul} radius="md"
/> shadow="sm"
<Text fw={"bold"}>Content</Text> w={{ base: '100%', md: '50%' }}
<EdukasiLingkunganTextEditor style={{ border: '1px solid #e0e0e0' }}
showSubmit={false} >
onChange={setContent} <Stack gap="md">
initialContent={content} <Box>
/> <Text fw="bold" mb={6}>
<Group> Judul
<Button </Text>
bg={colors['blue-button']} <EdukasiLingkunganTextEditor
onClick={submit} showSubmit={false}
loading={tujuanEdukasiState.update.loading} onChange={setJudul}
> initialContent={judul}
Submit />
</Button> </Box>
</Group>
</Stack> <Box>
</Paper> <Text fw="bold" mb={6}>
</Box> Konten
</Stack> </Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={tujuanEdukasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } 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';
@@ -8,47 +7,68 @@ import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan'; import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listTujuanEdukasi = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi) const listTujuanEdukasi = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi);
useShallowEffect(() => { useShallowEffect(() => {
listTujuanEdukasi.findById.load('edit') listTujuanEdukasi.findById.load('edit');
}, []) }, []);
if (!listTujuanEdukasi.findById.data) { if (!listTujuanEdukasi.findById.data) {
return ( return (
<Stack> <Stack py={20}>
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={600} />
</Stack> </Stack>
) );
} }
return ( return (
<Paper bg={colors['white-1']} p={'md'} radius={10}> <Box p="md">
<Stack gap={"22"}> <Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid> <Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Tujuan Edukasi Lingkungan</Title> <Title order={3} fw={600}>Preview Tujuan Edukasi Lingkungan</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')}> <Button
<IconEdit size={16} /> size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')
}
>
Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
<Box>
<Stack gap={'lg'}> <Stack gap="md">
<Paper p={"xl"} bg={colors['BG-trans']}> <Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box px={{ base: 0, md: 30 }}> <Box mb="md">
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.judul }} /> <Text
</Box> fz={{ base: 'xl', md: '2xl' }}
<Box px={{ base: 0, md: 30 }}> fw={600}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.deskripsi }} /> c='black'
</Box> dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.judul }}
</Paper> />
</Stack> </Box>
</Box> <Box>
</Stack> <Text
</Paper> fz={{ base: 'md', md: 'lg' }}
) ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -1,62 +1,121 @@
/* 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 { ScrollArea, 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 { IconClipboardList, IconTags } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "Kegiatan Desa",
value: "kegiatanDesa",
href: "/admin/lingkungan/gotong-royong/kegiatan-desa"
},
{
label: "Kategori Kegiatan",
value: "kategoriKegiatan",
href: "/admin/lingkungan/gotong-royong/kategori-kegiatan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "Kegiatan Desa",
router.push(tab.href) value: "kegiatanDesa",
} href: "/admin/lingkungan/gotong-royong/kegiatan-desa",
setActiveTab(value) icon: <IconClipboardList size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola kegiatan desa",
},
{
label: "Kategori Kegiatan",
value: "kategoriKegiatan",
href: "/admin/lingkungan/gotong-royong/kategori-kegiatan",
icon: <IconTags size={18} stroke={1.8} />,
tooltip: "Kelola kategori kegiatan desa",
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
} }
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}>Gotong Royong</Title> {/* ✅ Title lebih tegas */}
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}> Gotong Royong
{tabs.map((e, i) => ( </Title>
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))} <Tabs
</TabsList> color={colors['blue-button']}
{tabs.map((e, i) => ( variant="pills"
<TabsPanel key={i} value={e.value}> value={activeTab}
{/* Konten dummy, bisa diganti tergantung routing */} onChange={handleTabChange}
<></> radius="lg"
</TabsPanel> keepMounted={false}
))} >
</Tabs> {/* ✅ Scroll horizontal */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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>
</ScrollArea>
{/* ✅ Panel dengan gaya kartu */}
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children} {children}
</Stack> </TabsPanel>
); ))}
</Tabs>
</Stack>
);
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
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';
@@ -16,7 +16,7 @@ function EditKategoriKegiatan() {
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan); const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: "", nama: '',
}); });
useEffect(() => { useEffect(() => {
@@ -27,15 +27,14 @@ function EditKategoriKegiatan() {
const data = await stateKategori.edit.load(id); const data = await stateKategori.edit.load(id);
if (data) { if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id; stateKategori.edit.id = id;
setFormData({ setFormData({
nama: data.nama || '', nama: data.nama || '',
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading kategori kegiatan:", error); console.error('Error loading kategori kegiatan:', error);
toast.error("Gagal memuat data kategori kegiatan"); toast.error('Gagal memuat data kategori kegiatan');
} }
}; };
@@ -49,45 +48,63 @@ function EditKategoriKegiatan() {
return; return;
} }
stateKategori.edit.form = { stateKategori.edit.form = { nama: formData.nama.trim() };
nama: formData.nama.trim(), if (!stateKategori.edit.id) stateKategori.edit.id = id;
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
}
const success = await stateKategori.edit.update(); const success = await stateKategori.edit.update();
if (success) { if (success) {
router.push("/admin/lingkungan/gotong-royong/kategori-kegiatan"); toast.success('Kategori kegiatan berhasil diperbarui!');
router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan');
} }
} catch (error) { } catch (error) {
console.error("Error updating kategori kegiatan:", error); console.error('Error updating kategori kegiatan:', error);
// toast akan ditampilkan dari fungsi update
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md" align="center">
<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" p="xs" radius="md" onClick={() => router.back()}>
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Kegiatan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Kategori kegiatan</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori kegiatan</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>}
placeholder='Masukkan nama kategori kegiatan' placeholder="Masukkan nama kategori kegiatan"
required
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, 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 { useEffect } from 'react'; import { useEffect } from 'react';
@@ -29,31 +29,53 @@ function CreateKategoriKegiatan() {
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box> {/* Header */}
<Box mb={10}> <Group mb="md" align="center">
<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" p="xs" radius="md" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Kegiatan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Kegiatan</Title> w={{ base: '100%', md: '50%' }}
<TextInput bg={colors['white-1']}
value={stateKategori.create.form.nama} p="lg"
onChange={(val) => { radius="md"
stateKategori.create.form.nama = val.target.value; shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={stateKategori.create.form.nama}
onChange={(val) => (stateKategori.create.form.nama = val.target.value)}
label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>}
placeholder="Masukkan nama kategori kegiatan"
required
/>
<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)',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Kegiatan</Text>} >
placeholder='Masukkan nama kategori kegiatan' Simpan
/> </Button>
<Group> </Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> </Stack>
</Group> </Paper>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,24 +1,40 @@
'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 } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import gotongRoyongState from '../../../_state/lingkungan/gotong-royong'; import gotongRoyongState from '../../../_state/lingkungan/gotong-royong';
function KategoriKegiatan() { function KategoriKegiatan() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Kegiatan' title='Kategori Kegiatan'
placeholder='pencarian' placeholder='Cari kategori kegiatan...'
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,6 +50,14 @@ function ListKategoriKegiatan({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = stateKategori.findMany
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateKategori.delete.byId(selectedId) stateKategori.delete.byId(selectedId)
@@ -43,61 +67,110 @@ function ListKategoriKegiatan({ search }: { search: string }) {
} }
useShallowEffect(() => { useShallowEffect(() => {
stateKategori.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (stateKategori.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!stateKategori.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">
<JudulList <Group justify="space-between" mb="md">
title='List Kategori Kegiatan' <Title order={4}>Daftar Kategori Kegiatan</Title>
href='/admin/lingkungan/gotong-royong/kategori-kegiatan/create' <Tooltip label="Tambah Kategori Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Kategori</TableTh> onClick={() => router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan/create')}
<TableTh>Edit</TableTh> >
<TableTh>Delete</TableTh> Tambah Baru
</TableTr> </Button>
</TableThead> </Tooltip>
<TableTbody> </Group>
{filteredData.map((item) => (
<TableTr key={item.id}> <Box style={{ overflowX: 'auto' }}>
<TableTd>{item.nama}</TableTd> <Table highlightOnHover>
<TableTd> <TableThead>
<Button color="green" onClick={() => router.push(`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`)}> <TableTr>
<IconEdit size={20} /> <TableTh style={{ width: '60%' }}>Nama Kategori</TableTh>
</Button> <TableTh style={{ width: '20%' }}>Edit</TableTh>
</TableTd> <TableTh style={{ width: '20%' }}>Delete</TableTh>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() => router.push(`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`)}
>
Edit
</Button>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}
>
Hapus
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text c="dimmed">Tidak ada kategori kegiatan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -4,7 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
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';
@@ -12,7 +12,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';
interface FormKegiatanDesa { interface FormKegiatanDesa {
judul: string; judul: string;
deskripsiSingkat: string; deskripsiSingkat: string;
@@ -92,14 +91,11 @@ function EditGotongRoyong() {
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
kegiatanDesaState.edit.form.imageId = uploaded.id; kegiatanDesaState.edit.form.imageId = uploaded.id;
} }
await kegiatanDesaState.edit.update() await kegiatanDesaState.edit.update()
toast.success("Kegiatan desa berhasil diperbarui!")
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa"); router.push("/admin/lingkungan/gotong-royong/kegiatan-desa");
} catch (error) { } catch (error) {
console.error("Error updating kegiatan desa:", error); console.error("Error updating kegiatan desa:", error);
@@ -108,27 +104,40 @@ function EditGotongRoyong() {
} }
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">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kegiatan Desa
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Paper
<Stack gap="xs"> w={{ base: '100%', md: '50%' }}
<Title order={3}>Edit Kegiatan Desa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={formData.judul} value={formData.judul}
label={<Text fz="sm" fw="bold">Judul Kegiatan Desa</Text>} label={<Text fz="sm" fw="bold">Judul Kegiatan Desa</Text>}
placeholder="masukkan judul kegiatan desa" placeholder="masukkan judul kegiatan desa"
onChange={(e) => setFormData({ ...formData, judul: e.target.value })} onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
required
/> />
<TextInput <TextInput
value={formData.deskripsiSingkat} value={formData.deskripsiSingkat}
label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>} label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>}
placeholder="masukkan deskripsi singkat kegiatan desa" placeholder="masukkan deskripsi singkat kegiatan desa"
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })} onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
required
/> />
<Select <Select
label="Kategori Kegiatan" label="Kategori Kegiatan"
@@ -141,7 +150,7 @@ function EditGotongRoyong() {
/> />
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lengkap Kegiatan Desa</Text> <Text fw="bold" fz="sm">Deskripsi Lengkap Kegiatan Desa</Text>
<EditEditor <EditEditor
value={formData.deskripsiLengkap} value={formData.deskripsiLengkap}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -150,6 +159,7 @@ function EditGotongRoyong() {
}} }}
/> />
</Box> </Box>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>} label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>}
placeholder="masukkan tanggal kegiatan desa" placeholder="masukkan tanggal kegiatan desa"
@@ -169,71 +179,70 @@ function EditGotongRoyong() {
placeholder="masukkan partisipan kegiatan desa" placeholder="masukkan partisipan kegiatan desa"
onChange={(e) => { onChange={(e) => {
const val = Number(e.target.value); const val = Number(e.target.value);
if (!isNaN(val)) { if (!isNaN(val)) setFormData({ ...formData, partisipan: val });
setFormData({ ...formData, partisipan: val });
}
}} }}
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>Gambar Kegiatan Desa</Text>
<Box> <Dropzone
<Dropzone onDrop={(files) => {
onDrop={(files) => { const selectedFile = files[0];
const selectedFile = files[0]; // Ambil file pertama if (selectedFile) {
if (selectedFile) { setFile(selectedFile);
setFile(selectedFile); setPreviewImage(URL.createObjectURL(selectedFile));
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview }
} }}
}} onReject={() => toast.error('File tidak valid.')}
onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2}
maxSize={5 * 1024 ** 2} // Maks 5MB accept={{ 'image/*': [] }}
accept={{ 'image/*': [] }} radius="md"
> p="xl"
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> >
<Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Accept> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
<Dropzone.Reject> </Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Reject> <IconX size={48} color="red" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Idle> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>Drag gambar atau klik untuk pilih file</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar wajib</Text>
</Stack>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
</div> loading="lazy"
</Group> />
</Dropzone> </Box>
)}
{/* 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',
}}
loading="lazy"
/>
</Box>
)}
</Box>
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit} >
Simpan <Group justify="right">
</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>

View File

@@ -1,121 +1,166 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, 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, IconX } 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 colors from '@/con/colors'; import colors from '@/con/colors';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailKegiatanDesa() { function DetailKegiatanDesa() {
const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa) const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa);
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(() => {
kegiatanDesaState.findUnique.load(params?.id as string) kegiatanDesaState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
kegiatanDesaState.delete.byId(selectedId) kegiatanDesaState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa") router.push("/admin/lingkungan/gotong-royong/kegiatan-desa");
} }
} };
if (!kegiatanDesaState.findUnique.data) { if (!kegiatanDesaState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = kegiatanDesaState.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"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Kegiatan Desa Inovasi</Text> Kembali
{kegiatanDesaState.findUnique.data ? ( </Button>
<Paper key={kegiatanDesaState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Container Detail */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Judul Kegiatan Desa Inovasi</Text> withBorder
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.judul}</Text> w={{ base: "100%", md: "60%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Tanggal</Text> radius="md"
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.tanggal shadow="sm"
? new Date(kegiatanDesaState.findUnique.data.tanggal).toLocaleDateString() >
: "-"}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail Kegiatan Desa
<Text fw={"bold"} fz={"lg"}>Deskripsi Singkat</Text> </Text>
<Text fz={"lg"} >{kegiatanDesaState.findUnique.data?.deskripsiSingkat}</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> {/* Judul */}
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kegiatanDesaState.findUnique.data?.deskripsiLengkap }} /> <Box>
</Box> <Text fz="lg" fw="bold">Judul Kegiatan Desa Inovasi</Text>
<Box> <Text fz="md" c="dimmed">{data.judul || '-'}</Text>
<Text fw={"bold"} fz={"lg"}>Kategori Kegiatan</Text> </Box>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.kategoriKegiatan?.nama}</Text>
</Box> {/* Tanggal */}
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Partisipan</Text> <Text fz="lg" fw="bold">Tanggal</Text>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.partisipan}</Text> <Text fz="md" c="dimmed">
</Box> {data.tanggal ? new Date(data.tanggal).toLocaleDateString() : '-'}
<Box> </Text>
<Text fw={"bold"} fz={"lg"}>Lokasi</Text> </Box>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.lokasi}</Text>
</Box> {/* Deskripsi Singkat */}
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text> <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={kegiatanDesaState.findUnique.data?.image?.link} alt="gambar" loading="lazy" /> <Text fz="md" c="dimmed">{data.deskripsiSingkat || '-'}</Text>
</Box> </Box>
<Flex gap={"xs"} mt={10}>
{/* Deskripsi Lengkap */}
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} />
</Box>
{/* Kategori */}
<Box>
<Text fz="lg" fw="bold">Kategori Kegiatan</Text>
<Text fz="md" c="dimmed">{data.kategoriKegiatan?.nama || '-'}</Text>
</Box>
{/* Partisipan */}
<Box>
<Text fz="lg" fw="bold">Partisipan</Text>
<Text fz="md" c="dimmed">{data.partisipan || '-'}</Text>
</Box>
{/* Lokasi */}
<Box>
<Text fz="lg" fw="bold">Lokasi</Text>
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
</Box>
{/* Gambar */}
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.judul || 'Gambar Kegiatan Desa'}
w={150}
h={150}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Tombol Hapus & Edit */}
<Flex gap="sm" mt={10}>
<Tooltip label="Hapus Kegiatan Desa" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (kegiatanDesaState.findUnique.data) { setSelectedId(data.id);
setSelectedId(kegiatanDesaState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={kegiatanDesaState.delete.loading || !kegiatanDesaState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Kegiatan Desa" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (kegiatanDesaState.findUnique.data) { onClick={() => router.push(`/admin/lingkungan/gotong-royong/kegiatan-desa/${data.id}/edit`)}
router.push(`/admin/lingkungan/gotong-royong/kegiatan-desa/${kegiatanDesaState.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!kegiatanDesaState.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Flex>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -130,4 +175,4 @@ function DetailKegiatanDesa() {
); );
} }
export default DetailKegiatanDesa; export default DetailKegiatanDesa;

View File

@@ -4,7 +4,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
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,10 +24,9 @@ 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 CreateKegiatanDesa() { function CreateKegiatanDesa() {
const router = useRouter(); const router = useRouter();
const stateKegiatanDesa = useProxy(gotongRoyongState.kegiatanDesa) const stateKegiatanDesa = useProxy(gotongRoyongState.kegiatanDesa);
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);
@@ -26,32 +37,33 @@ function CreateKegiatanDesa() {
const resetForm = () => { const resetForm = () => {
stateKegiatanDesa.create.form = { stateKegiatanDesa.create.form = {
judul: "", judul: '',
deskripsiSingkat: "", deskripsiSingkat: '',
deskripsiLengkap: "", deskripsiLengkap: '',
tanggal: new Date(), tanggal: new Date(),
lokasi: "", lokasi: '',
partisipan: 0, partisipan: 0,
imageId: "", imageId: '',
kategoriKegiatanId: "", kategoriKegiatanId: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
stateKegiatanDesa.create.form.imageId = uploaded.id; stateKegiatanDesa.create.form.imageId = uploaded.id;
@@ -59,153 +71,176 @@ function CreateKegiatanDesa() {
await stateKegiatanDesa.create.create(); await stateKegiatanDesa.create.create();
resetForm(); resetForm();
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa") router.push('/admin/lingkungan/gotong-royong/kegiatan-desa');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<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> <Button
</Box> 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 Kegiatan Desa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kegiatan Desa</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Kegiatan
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ textAlign: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> loading="lazy"
</Group> />
</Dropzone> </Box>
)}
{/* 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',
}}
loading="lazy"
/>
</Box>
)}
</Box>
</Box> </Box>
{/* Input Form */}
<TextInput <TextInput
label="Judul Kegiatan"
placeholder="Masukkan judul kegiatan"
value={stateKegiatanDesa.create.form.judul} value={stateKegiatanDesa.create.form.judul}
onChange={(val) => { onChange={(e) => (stateKegiatanDesa.create.form.judul = e.target.value)}
stateKegiatanDesa.create.form.judul = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Kegiatan</Text>}
placeholder='Masukkan judul kegiatan'
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={stateKegiatanDesa.create.form.deskripsiSingkat} value={stateKegiatanDesa.create.form.deskripsiSingkat}
onChange={(val) => { onChange={(e) => (stateKegiatanDesa.create.form.deskripsiSingkat = e.target.value)}
stateKegiatanDesa.create.form.deskripsiSingkat = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi singkat'
/> />
<TextInput <TextInput
type="number" type="number"
min={0} min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={stateKegiatanDesa.create.form.partisipan} value={stateKegiatanDesa.create.form.partisipan}
onChange={(val) => { onChange={(e) => {
const value = Number(val.target.value); const value = Number(e.target.value);
if (value >= 0) {
// Validasi manual juga boleh (jaga-jaga)
if (value >= 0 && value <= 1000) {
stateKegiatanDesa.create.form.partisipan = value; stateKegiatanDesa.create.form.partisipan = value;
} }
}} }}
label={<Text fw={"bold"} fz={"sm"}>Partisipan</Text>} label="Partisipan"
placeholder='Masukkan partisipan' placeholder="Masukkan jumlah partisipan"
required
/> />
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>} label="Tanggal"
type="date" type="date"
placeholder="Contoh: 2022-01-01" placeholder="Contoh: 2022-01-01"
value={stateKegiatanDesa.create.form.tanggal ? stateKegiatanDesa.create.form.tanggal.toISOString().split('T')[0] : ''} value={
stateKegiatanDesa.create.form.tanggal
? stateKegiatanDesa.create.form.tanggal.toISOString().split('T')[0]
: ''
}
onChange={(e) => { onChange={(e) => {
const dateValue = e.currentTarget.value; const dateValue = e.currentTarget.value;
stateKegiatanDesa.create.form.tanggal = dateValue ? new Date(dateValue) : new Date(); stateKegiatanDesa.create.form.tanggal = dateValue ? new Date(dateValue) : new Date();
}} }}
required
/> />
<TextInput <TextInput
label="Lokasi"
placeholder="Masukkan lokasi kegiatan"
value={stateKegiatanDesa.create.form.lokasi} value={stateKegiatanDesa.create.form.lokasi}
onChange={(val) => { onChange={(e) => (stateKegiatanDesa.create.form.lokasi = e.target.value)}
stateKegiatanDesa.create.form.lokasi = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Lengkap</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi Lengkap
</Text>
<CreateEditor <CreateEditor
value={stateKegiatanDesa.create.form.deskripsiLengkap} value={stateKegiatanDesa.create.form.deskripsiLengkap}
onChange={(val) => { onChange={(val) => (stateKegiatanDesa.create.form.deskripsiLengkap = val)}
stateKegiatanDesa.create.form.deskripsiLengkap = val;
}}
/> />
</Box> </Box>
<Select <Select
label="Kategori Kegiatan"
placeholder="Pilih kategori kegiatan"
value={stateKegiatanDesa.create.form.kategoriKegiatanId} value={stateKegiatanDesa.create.form.kategoriKegiatanId}
onChange={(val) => { onChange={(val) => (stateKegiatanDesa.create.form.kategoriKegiatanId = val ?? '')}
stateKegiatanDesa.create.form.kategoriKegiatanId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Kegiatan</Text>}
placeholder="Pilih kategori produk"
data={ data={
gotongRoyongState.kategoriKegiatan.findMany.data?.map((v) => ({ gotongRoyongState.kategoriKegiatan.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.nama, label: v.nama,
})) || [] })) || []
} }
required
/> />
<Group> {/* Submit */}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,13 +1,29 @@
/* 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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import HeaderSearch from '../../../_com/header'; import { useState } from 'react';
import JudulList from '../../../_com/judulList';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import gotongRoyongState from '../../../_state/lingkungan/gotong-royong'; import gotongRoyongState from '../../../_state/lingkungan/gotong-royong';
function KegiatanDesa() { function KegiatanDesa() {
@@ -15,8 +31,8 @@ function KegiatanDesa() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kegiatan Desa' title="Kegiatan Desa"
placeholder='pencarian' placeholder="Cari judul, lokasi, atau kategori kegiatan..."
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,71 +43,123 @@ function KegiatanDesa() {
} }
function ListKegiatanDesa({ search }: { search: string }) { function ListKegiatanDesa({ search }: { search: string }) {
const listState = useProxy(gotongRoyongState.kegiatanDesa) const listState = useProxy(gotongRoyongState.kegiatanDesa);
const router = useRouter(); const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => { const {
const keyword = search.toLowerCase(); data,
return ( page,
item.judul.toLowerCase().includes(keyword) || totalPages,
item.lokasi.toLowerCase().includes(keyword) || loading,
item.kategoriKegiatan?.nama?.toLowerCase().includes(keyword) load,
); } = listState.findMany;
});
if (!listState.findMany.data) { useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = 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 Kegiatan Desa</Title>
title='List Kegiatan Desa' <Button
href='/admin/lingkungan/gotong-royong/kegiatan-desa/create' leftSection={<IconPlus size={18} />}
/> color="blue"
<Box style={{ overflowX: 'auto' }}> variant="light"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> onClick={() =>
<TableThead> router.push('/admin/lingkungan/gotong-royong/kegiatan-desa/create')
<TableTr> }
<TableTh>Judul Kegiatan Desa</TableTh> >
<TableTh>Kategori Kegiatan Desa</TableTh> Tambah Baru
<TableTh>Lokasi Kegiatan Desa</TableTh> </Button>
<TableTh>Detail</TableTh> </Group>
</TableTr> <Box style={{ overflowX: 'auto' }}>
</TableThead> <Table highlightOnHover>
<TableTbody> <TableThead>
{filteredData.map((item) => ( <TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Lokasi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{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.kategoriKegiatan?.nama}</TableTd>
<TableTd>{item.lokasi}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/gotong-royong/kegiatan-desa/${item.id}`)}> <Text fz="sm" c="dimmed">
<IconDeviceImacCog size={25} /> {item.kategoriKegiatan?.nama || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="sm">{item.lokasi}</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/lingkungan/gotong-royong/kegiatan-desa/${item.id}`
)
}
>
Detail
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text c="dimmed">
Tidak ada kegiatan desa yang cocok dengan pencarian
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box> </Box>
) );
} }
export default KegiatanDesa; export default KegiatanDesa;

View File

@@ -1,67 +1,116 @@
/* 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 { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBook, IconLeaf, IconSchool } 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';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const tabs = [ const tabs = [
{ {
label: "Filosofi Tri Hita", label: "Filosofi Tri Hita",
value: "filosofi-tri-hita", value: "filosofi-tri-hita",
href: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana" href: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana",
tooltip: "Lihat filosofi Tri Hita Karana",
icon: <IconLeaf size={18} stroke={1.8} />
}, },
{ {
label: "Nilai Konservasi Adat", label: "Nilai Konservasi Adat",
value: "nilai-konservasi-adat", value: "nilai-konservasi-adat",
href: "/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat" href: "/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat",
tooltip: "Kelola nilai konservasi adat",
icon: <IconBook size={18} stroke={1.8} />
}, },
{ {
label: "Bentuk Konservasi Berdasarkan Adat", label: "Bentuk Konservasi Berdasarkan Adat",
value: "bentuk-konservasi-berdasarkan-adat", value: "bentuk-konservasi-berdasarkan-adat",
href: "/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat" href: "/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat",
tooltip: "Lihat bentuk konservasi berdasarkan adat",
icon: <IconSchool size={18} stroke={1.8} />
}, },
]; ];
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="md">
<Title order={3}>Konservasi Adat Bali</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Konservasi Adat Bali
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> <Tabs
))} color={colors['blue-button']}
</TabsList> variant="pills"
{tabs.map((e, i) => ( value={activeTab}
<TabsPanel key={i} value={e.value}> onChange={handleTabChange}
{/* Konten dummy, bisa diganti tergantung routing */} radius="lg"
<></> keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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",
whiteSpace: "nowrap",
transition: "all 0.2s ease",
}}
>
<span style={{
display: "inline-block",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}>
{tab.label}
</span>
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel key={i} value={tab.value}>
<Box p="md">
{children}
</Box>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -9,77 +9,105 @@ 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';
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), { const KonservasiAdatBaliTextEditor = dynamic(
ssr: false, () => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor),
}); { ssr: false }
);
function EditBentukKonservasiBerdasarkanAdat() { function EditBentukKonservasiBerdasarkanAdat() {
const router = useRouter() const router = useRouter();
const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat) const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat);
const [judul, setJudul] = useState(''); const [judul, setJudul] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useShallowEffect(() => { useShallowEffect(() => {
if (!bentukKonservasiState.findById.data) { if (!bentukKonservasiState.findById.data) {
bentukKonservasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu bentukKonservasiState.findById.initialize();
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (bentukKonservasiState.findById.data) { if (bentukKonservasiState.findById.data) {
setJudul(bentukKonservasiState.findById.data.judul ?? '') setJudul(bentukKonservasiState.findById.data.judul ?? '');
setContent(bentukKonservasiState.findById.data.deskripsi ?? '') setContent(bentukKonservasiState.findById.data.deskripsi ?? '');
} }
}, [bentukKonservasiState.findById.data]) }, [bentukKonservasiState.findById.data]);
const submit = () => { const submit = () => {
if (bentukKonservasiState.findById.data) { if (bentukKonservasiState.findById.data) {
bentukKonservasiState.findById.data.judul = judul; bentukKonservasiState.findById.data.judul = judul;
bentukKonservasiState.findById.data.deskripsi = content; bentukKonservasiState.findById.data.deskripsi = content;
bentukKonservasiState.update.save(bentukKonservasiState.findById.data) bentukKonservasiState.update.save(bentukKonservasiState.findById.data);
} }
router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat') router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap={'xs'}> {/* Header */}
<Box> <Group mb="md">
<Button <Button
variant={'subtle'} variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
> p="xs"
<IconArrowBack color={colors['blue-button']} size={20} /> radius="md"
</Button> >
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Box> </Button>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> <Title order={4} ml="sm" c="dark">
<Stack gap={'xs'}> Edit Bentuk Konservasi Berdasarkan Adat
<Title order={3}>Edit Bentuk Konservasi Berdasarkan Adat</Title> </Title>
<Text fw={"bold"}>Judul</Text> </Group>
<KonservasiAdatBaliTextEditor
showSubmit={false} {/* Form Paper */}
onChange={setJudul} <Paper
initialContent={judul} bg={colors['white-1']}
/> p="lg"
<Text fw={"bold"}>Content</Text> radius="md"
<KonservasiAdatBaliTextEditor shadow="sm"
showSubmit={false} w={{ base: '100%', md: '50%' }}
onChange={setContent} style={{ border: '1px solid #e0e0e0' }}
initialContent={content} >
/> <Stack gap="md">
<Group> <Box>
<Button <Text fw="bold" mb={6}>
bg={colors['blue-button']} Judul
onClick={submit} </Text>
loading={bentukKonservasiState.update.loading} <KonservasiAdatBaliTextEditor
> showSubmit={false}
Submit onChange={setJudul}
</Button> initialContent={judul}
</Group> />
</Stack> </Box>
</Paper>
</Box> <Box>
</Stack> <Text fw="bold" mb={6}>
Deskripsi
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={bentukKonservasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -8,47 +8,80 @@ import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali'; import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listBentukKonservasiBerdasarkanAdat = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat) const listBentukKonservasiBerdasarkanAdat = useProxy(
stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat
);
useShallowEffect(() => { useShallowEffect(() => {
listBentukKonservasiBerdasarkanAdat.findById.load('edit') listBentukKonservasiBerdasarkanAdat.findById.load('edit');
}, []) }, []);
if (!listBentukKonservasiBerdasarkanAdat.findById.data) { if (!listBentukKonservasiBerdasarkanAdat.findById.data) {
return ( return (
<Stack> <Stack py={20} align="center">
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={600} width="100%" />
</Stack> </Stack>
) );
} }
return ( return (
<Paper bg={colors['white-1']} p={'md'} radius={10}> <Box p={{ base: 'md', md: 'xl' }}>
<Stack gap={"22"}> <Paper withBorder radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}>
<Grid> {/* Header */}
<Grid align="center" mb="lg">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Bentuk Konservasi Berdasarkan Adat</Title> <Title order={3} fw={600} c="dark">
Preview Bentuk Konservasi Berdasarkan Adat
</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit')}> <Button
<IconEdit size={16} /> size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
'/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit'
)
}
>
Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
<Box>
<Stack gap={'lg'}> {/* Konten */}
<Paper p={"xl"} bg={colors['BG-trans']}> <Stack gap="md">
<Box px={{ base: 0, md: 30 }}> <Paper radius="md" p={{ base: 'md', md: 'xl' }} bg={colors['BG-trans']} shadow="sm">
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listBentukKonservasiBerdasarkanAdat.findById.data.judul }} /> <Box mb="md">
</Box> <Text
<Box px={{ base: 0, md: 30 }}> fz={{ base: 'xl', md: '2xl' }}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listBentukKonservasiBerdasarkanAdat.findById.data.deskripsi }} /> fw={600}
</Box> c="dark"
</Paper> dangerouslySetInnerHTML={{
</Stack> __html: listBentukKonservasiBerdasarkanAdat.findById.data.judul,
</Box> }}
</Stack> />
</Paper> </Box>
) <Box>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{
__html: listBentukKonservasiBerdasarkanAdat.findById.data.deskripsi,
}}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -9,78 +9,108 @@ 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';
const KonservasiAdatBaliTextEditor = dynamic(
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), { () =>
ssr: false, import('../../_lib/konservasiAdatBaliTextEditor').then(
}); (mod) => mod.KonservasiAdatBaliTextEditor
),
{ ssr: false }
);
function EditFilosofiTriHitaKarana() { function EditFilosofiTriHitaKarana() {
const router = useRouter() const router = useRouter();
const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita) const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita);
const [judul, setJudul] = useState(''); const [judul, setJudul] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useShallowEffect(() => { useShallowEffect(() => {
if (!filosofiTriHitaState.findById.data) { if (!filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.initialize(); // biar masuk ke `findFirst` route kamu filosofiTriHitaState.findById.initialize();
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (filosofiTriHitaState.findById.data) { if (filosofiTriHitaState.findById.data) {
setJudul(filosofiTriHitaState.findById.data.judul ?? '') setJudul(filosofiTriHitaState.findById.data.judul ?? '');
setContent(filosofiTriHitaState.findById.data.deskripsi ?? '') setContent(filosofiTriHitaState.findById.data.deskripsi ?? '');
} }
}, [filosofiTriHitaState.findById.data]) }, [filosofiTriHitaState.findById.data]);
const submit = () => { const submit = () => {
if (filosofiTriHitaState.findById.data) { if (filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.data.judul = judul; filosofiTriHitaState.findById.data.judul = judul;
filosofiTriHitaState.findById.data.deskripsi = content; filosofiTriHitaState.findById.data.deskripsi = content;
filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data) filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data);
} }
router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana') router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap={'xs'}> {/* Header */}
<Box> <Group mb="md">
<Button <Button
variant={'subtle'} variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
> p="xs"
<IconArrowBack color={colors['blue-button']} size={20} /> radius="md"
</Button> >
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Box> </Button>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> <Title order={4} ml="sm" c="dark">
<Stack gap={'xs'}> Edit Filosofi Tri Hita Karana
<Title order={3}>Edit Filosofi Tri Hita Karana</Title> </Title>
<Text fw={"bold"}>Judul</Text> </Group>
<KonservasiAdatBaliTextEditor
showSubmit={false} {/* Form Paper */}
onChange={setJudul} <Paper
initialContent={judul} bg={colors['white-1']}
/> p="lg"
<Text fw={"bold"}>Content</Text> radius="md"
<KonservasiAdatBaliTextEditor shadow="sm"
showSubmit={false} w={{ base: '100%', md: '50%' }}
onChange={setContent} style={{ border: '1px solid #e0e0e0' }}
initialContent={content} >
/> <Stack gap="md">
<Group> <Box>
<Button <Text fw="bold" mb={6}>
bg={colors['blue-button']} Judul
onClick={submit} </Text>
loading={filosofiTriHitaState.update.loading} <KonservasiAdatBaliTextEditor
> showSubmit={false}
Submit onChange={setJudul}
</Button> initialContent={judul}
</Group> />
</Stack> </Box>
</Paper>
</Box> <Box>
</Stack> <Text fw="bold" mb={6}>
Deskripsi
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={filosofiTriHitaState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } 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';
@@ -7,49 +6,73 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali'; import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listFilosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita) const listFilosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita);
useShallowEffect(() => { useShallowEffect(() => {
listFilosofi.findById.load('edit') listFilosofi.findById.load('edit');
}, []) }, []);
if (!listFilosofi.findById.data) { if (!listFilosofi.findById.data) {
return ( return (
<Stack> <Stack py={20}>
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={600} />
</Stack> </Stack>
) );
} }
return ( return (
<Paper bg={colors['white-1']} p={'md'} radius={10}> <Box p="md">
<Stack gap={"22"}> <Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid> <Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Filosofi Tri Hita Karana</Title> <Title order={3} fw={600}>
Preview Filosofi Tri Hita Karana
</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit')}> <Button
<IconEdit size={16} /> size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
'/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit'
)
}
>
Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
<Box>
<Stack gap={'lg'}> <Stack gap="md">
<Paper p={"xl"} bg={colors['BG-trans']}> <Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box px={{ base: 0, md: 30 }}> <Box mb="md" px={{ base: 0, md: 20 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.judul }} /> <Text
</Box> fz={{ base: 'xl', md: '2xl' }}
<Box px={{ base: 0, md: 30 }}> fw={600}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.deskripsi }} /> c="black"
</Box> dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.judul }}
</Paper> />
</Stack> </Box>
</Box> <Box px={{ base: 0, md: 20 }}>
</Stack> <Text
</Paper> fz={{ base: 'md', md: 'lg' }}
) ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -9,77 +9,105 @@ 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';
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), { const KonservasiAdatBaliTextEditor = dynamic(
ssr: false, () => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor),
}); { ssr: false }
);
function EditNilaiKonservasiAdat() { function EditNilaiKonservasiAdat() {
const router = useRouter() const router = useRouter();
const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat) const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat);
const [judul, setJudul] = useState(''); const [judul, setJudul] = useState('');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
useShallowEffect(() => { useShallowEffect(() => {
if (!nilaiKonservasiState.findById.data) { if (!nilaiKonservasiState.findById.data) {
nilaiKonservasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu nilaiKonservasiState.findById.initialize();
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (nilaiKonservasiState.findById.data) { if (nilaiKonservasiState.findById.data) {
setJudul(nilaiKonservasiState.findById.data.judul ?? '') setJudul(nilaiKonservasiState.findById.data.judul ?? '');
setContent(nilaiKonservasiState.findById.data.deskripsi ?? '') setContent(nilaiKonservasiState.findById.data.deskripsi ?? '');
} }
}, [nilaiKonservasiState.findById.data]) }, [nilaiKonservasiState.findById.data]);
const submit = () => { const submit = () => {
if (nilaiKonservasiState.findById.data) { if (nilaiKonservasiState.findById.data) {
nilaiKonservasiState.findById.data.judul = judul; nilaiKonservasiState.findById.data.judul = judul;
nilaiKonservasiState.findById.data.deskripsi = content; nilaiKonservasiState.findById.data.deskripsi = content;
nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data) nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data);
} }
router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat') router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap={'xs'}> {/* Header */}
<Box> <Group mb="md">
<Button <Button
variant={'subtle'} variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
> p="xs"
<IconArrowBack color={colors['blue-button']} size={20} /> radius="md"
</Button> >
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Box> </Button>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> <Title order={4} ml="sm" c="dark">
<Stack gap={'xs'}> Edit Nilai Konservasi Adat
<Title order={3}>Edit Nilai Konservasi Adat</Title> </Title>
<Text fw={"bold"}>Judul</Text> </Group>
<KonservasiAdatBaliTextEditor
showSubmit={false} {/* Form Paper */}
onChange={setJudul} <Paper
initialContent={judul} bg={colors['white-1']}
/> p="lg"
<Text fw={"bold"}>Content</Text> radius="md"
<KonservasiAdatBaliTextEditor shadow="sm"
showSubmit={false} w={{ base: '100%', md: '50%' }}
onChange={setContent} style={{ border: '1px solid #e0e0e0' }}
initialContent={content} >
/> <Stack gap="md">
<Group> <Box>
<Button <Text fw="bold" mb={6}>
bg={colors['blue-button']} Judul
onClick={submit} </Text>
loading={nilaiKonservasiState.update.loading} <KonservasiAdatBaliTextEditor
> showSubmit={false}
Submit onChange={setJudul}
</Button> initialContent={judul}
</Group> />
</Stack> </Box>
</Paper>
</Box> <Box>
</Stack> <Text fw="bold" mb={6}>
Deskripsi
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={nilaiKonservasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } 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';
@@ -8,47 +7,70 @@ import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali'; import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listNilaiKonservasiAdat = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat) const listNilaiKonservasiAdat = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat);
useShallowEffect(() => { useShallowEffect(() => {
listNilaiKonservasiAdat.findById.load('edit') listNilaiKonservasiAdat.findById.load('edit');
}, []) }, []);
if (!listNilaiKonservasiAdat.findById.data) { if (!listNilaiKonservasiAdat.findById.data) {
return ( return (
<Stack> <Stack py={20}>
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={600} />
</Stack> </Stack>
) );
} }
return ( return (
<Paper bg={colors['white-1']} p={'md'} radius={10}> <Box p="md">
<Stack gap={"22"}> <Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid> <Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Nilai Konservasi Adat</Title> <Title order={3} fw={600}>
Preview Nilai Konservasi Adat
</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit')}> <Button
<IconEdit size={16} /> size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit')
}
>
Edit
</Button> </Button>
</GridCol> </GridCol>
</Grid> </Grid>
<Box>
<Stack gap={'lg'}> <Stack gap="md">
<Paper p={"xl"} bg={colors['BG-trans']}> <Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box px={{ base: 0, md: 30 }}> <Box mb="md">
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.judul }} /> <Text
</Box> fz={{ base: 'xl', md: '2xl' }}
<Box px={{ base: 0, md: 30 }}> fw={600}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.deskripsi }} /> c="black"
</Box> dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.judul }}
</Paper> />
</Stack> </Box>
</Box> <Box>
</Stack> <Text
</Paper> fz={{ base: 'md', md: 'lg' }}
) ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -2,8 +2,9 @@
'use client' 'use client'
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconTrash, IconRecycle } from '@tabler/icons-react'; import { IconTrash, IconRecycle } from '@tabler/icons-react';
import colors from '@/con/colors';
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) { function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
@@ -45,49 +46,75 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
}, [pathname]); }, [pathname]);
return ( return (
<Stack gap="md"> <Stack gap="lg">
<Title order={3} mb="sm">Pengelolaan Sampah Bank Sampah</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs Pengelolaan Sampah Bank Sampah
value={activeTab} </Title>
onChange={handleTabChange}
<Tabs
color={colors["blue-button"]}
variant="pills" variant="pills"
radius="md" value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
> >
<TabsList> {/* ✅ Scroll horizontal wrapper */}
{tabs.map((tab) => ( <ScrollArea type="auto" offsetScrollbars>
<Tooltip <TabsList
key={tab.value} p="sm"
label={tab.tooltip} style={{
position="top" background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
withArrow borderRadius: "1rem",
transitionProps={{ transition: 'pop', duration: 300 }} boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
> display: "flex",
<TabsTab flexWrap: "nowrap",
value={tab.value} gap: "0.5rem",
leftSection={tab.icon} paddingInline: "0.5rem",
style={{ }}
padding: '10px 20px', >
height: 'auto', {tabs.map((tab, i) => (
minHeight: 44, <Tooltip
}} key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
<TabsPanel fontSize: "0.9rem",
value={activeTab || ''} transition: "all 0.2s ease",
pt="lg" }}
style={{ >
minHeight: '60vh', {tab.label}
}} </TabsTab>
> </Tooltip>
{children} ))}
</TabsPanel> </TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
minHeight: "60vh",
}}
>
{children}
</TabsPanel>
))}
</Tabs> </Tabs>
</Stack> </Stack>
); );
} }
export default LayoutTabsPengelolaanSampahBankSampah; export default LayoutTabsPengelolaanSampahBankSampah;

View File

@@ -4,14 +4,23 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan'; import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
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';
interface FormProgramPenghijauan { interface FormProgramPenghijauan {
name: string; name: string;
deskripsi: string; deskripsi: string;
@@ -19,19 +28,32 @@ interface FormProgramPenghijauan {
icon: string; icon: string;
} }
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash' | 'lingkunganSehat' | 'sumberOksigen' | 'ekonomiBerkelanjutan' | 'mencegahBencana'; type IconKey =
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana';
function EditProgramPenghijauan() { function EditProgramPenghijauan() {
const stateProgramPenghijauan = useProxy(programPenghijauanState) const stateProgramPenghijauan = useProxy(programPenghijauanState);
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const [formData, setFormData] = useState<FormProgramPenghijauan>({ const [formData, setFormData] = useState<FormProgramPenghijauan>({
name: '', name: '',
deskripsi: '', deskripsi: '',
judul: '', judul: '',
icon: '', icon: '',
}) });
useEffect(() => { useEffect(() => {
const loadProgramPenghijauan = async () => { const loadProgramPenghijauan = async () => {
@@ -41,7 +63,6 @@ function EditProgramPenghijauan() {
try { try {
const data = await stateProgramPenghijauan.update.load(id); const data = await stateProgramPenghijauan.update.load(id);
if (data) { if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramPenghijauan.update.id = id; stateProgramPenghijauan.update.id = id;
stateProgramPenghijauan.update.form = { stateProgramPenghijauan.update.form = {
@@ -59,16 +80,14 @@ function EditProgramPenghijauan() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading program penghijauan:", error); console.error('Error loading program penghijauan:', error);
toast.error("Gagal memuat data program penghijauan"); toast.error('Gagal memuat data program penghijauan');
} }
} };
loadProgramPenghijauan(); loadProgramPenghijauan();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateProgramPenghijauan.update.form = { stateProgramPenghijauan.update.form = {
@@ -77,49 +96,74 @@ function EditProgramPenghijauan() {
deskripsi: formData.deskripsi.trim(), deskripsi: formData.deskripsi.trim(),
judul: formData.judul.trim(), judul: formData.judul.trim(),
icon: formData.icon.trim(), icon: formData.icon.trim(),
} };
await stateProgramPenghijauan.update.submit(); await stateProgramPenghijauan.update.submit();
router.push("/admin/lingkungan/program-penghijauan"); router.push('/admin/lingkungan/program-penghijauan');
} catch (error) { } catch (error) {
console.error("Error updating program penghijauan:", error); console.error('Error updating program penghijauan:', error);
toast.error("Gagal memuat data program penghijauan"); toast.error('Gagal memperbarui program penghijauan');
} }
} };
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={3}>Edit Program Penghijauan</Title> {/* Header dengan back button */}
<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">
Edit Program Penghijauan
</Title>
</Group>
{/* Card utama */}
<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={formData.name} value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Penghijauan</Text>} label="Nama Program Penghijauan"
placeholder="masukkan nama program penghijauan" placeholder="Masukkan nama program penghijauan"
onChange={(val) => { onChange={(val) =>
setFormData({ setFormData({
...formData, ...formData,
name: val.target.value name: val.target.value,
}) })
}} }
required
/> />
<TextInput <TextInput
value={formData.judul} value={formData.judul}
label={<Text fz={"sm"} fw={"bold"}>Judul Deskripsi Program Penghijauan</Text>} label="Judul Deskripsi Program Penghijauan"
placeholder="masukkan judul deskripsi program penghijauan" placeholder="Masukkan judul deskripsi program penghijauan"
onChange={(val) => { onChange={(val) =>
setFormData({ setFormData({
...formData, ...formData,
judul: val.target.value judul: val.target.value,
}) })
}} }
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -128,16 +172,35 @@ function EditProgramPenghijauan() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Penghijauan</Text> <Text fw="bold" fz="sm" mb={6}>
Ikon Program Penghijauan
</Text>
<SelectIconProgramEdit <SelectIconProgramEdit
value={formData.icon as IconKey} value={formData.icon as IconKey}
onChange={(value) => { onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value })); setFormData((prev) => ({ ...prev, icon: value }));
stateProgramPenghijauan.update.form.icon = value; stateProgramPenghijauan.update.form.icon = value;
}} /> }}
/>
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol simpan */}
<Group justify="flex-end">
<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>

View File

@@ -1,17 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { 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, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react'; import {
IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard,
IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale,
IconShieldFilled, IconTent, IconTrash, IconTrendingUp,
IconTrophy, IconTruck
} from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan'; import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramPenghijauan() { function DetailProgramPenghijauan() {
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const stateProgramPenghijauan = useProxy(programPenghijauanState) const stateProgramPenghijauan = useProxy(programPenghijauanState)
@@ -50,72 +53,97 @@ function DetailProgramPenghijauan() {
if (!stateProgramPenghijauan.findUnique.data) { if (!stateProgramPenghijauan.findUnique.data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( const data = stateProgramPenghijauan.findUnique.data
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Penghijauan</Text>
<Paper bg={colors['BG-trans']} p={'md'}> return (
<Stack gap={"xs"}> <Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Konten detail */}
<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 Program Penghijauan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Penghijauan</Text> <Text fz="lg" fw="bold">Nama Program</Text>
<Text fz={"lg"}>{stateProgramPenghijauan.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Penghijauan</Text> <Text fz="lg" fw="bold">Ikon Program</Text>
{iconMap[stateProgramPenghijauan.findUnique.data?.icon] && ( {iconMap[data?.icon] ? (
<Box title={stateProgramPenghijauan.findUnique.data?.icon}> <Box title={data?.icon}>
{React.createElement(iconMap[stateProgramPenghijauan.findUnique.data?.icon], { size: 24 })} {React.createElement(iconMap[data.icon], { size: 28, color: colors['blue-button'] })}
</Box> </Box>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)} )}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Judul Deskripsi Program Penghijauan</Text> <Text fz="lg" fw="bold">Judul Deskripsi</Text>
<Text fz={"lg"}>{stateProgramPenghijauan.findUnique.data?.judul}</Text> <Text fz="md" c="dimmed">{data?.judul || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Program Penghijauan</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramPenghijauan.findUnique.data?.deskripsi }}></Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box> </Box>
<Box>
<Flex gap={"xs"} mt={10}> {/* Tombol aksi */}
<Group gap="sm" mt="md">
<Tooltip label="Hapus Program" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateProgramPenghijauan.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateProgramPenghijauan.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={stateProgramPenghijauan.delete.loading || !stateProgramPenghijauan.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Program" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateProgramPenghijauan.findUnique.data) { onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${data.id}/edit`)}
router.push(`/admin/lingkungan/program-penghijauan/${stateProgramPenghijauan.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateProgramPenghijauan.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -126,7 +154,7 @@ function DetailProgramPenghijauan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program penghijauan ini?" text="Apakah Anda yakin ingin menghapus program penghijauan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,6 +1,16 @@
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
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';
@@ -8,59 +18,104 @@ import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon'; import SelectIconProgram from '../../../_com/selectIcon';
import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan'; import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan';
function CreateProgramPenghijauan() { function CreateProgramPenghijauan() {
const stateCreate = useProxy(programPenghijauanState) const stateCreate = useProxy(programPenghijauanState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateCreate.create.form = { stateCreate.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
judul: "", judul: '',
icon: "", icon: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/lingkungan/program-penghijauan") router.push('/admin/lingkungan/program-penghijauan');
} };
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={3}>Create Program Penghijauan</Title> {/* Tombol back + title */}
<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 Program Penghijauan
</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 Program Penghijauan</Text>} label={<Text fz="sm" fw="bold">Nama Program Penghijauan</Text>}
placeholder="masukkan nama program penghijauan" placeholder="Masukkan nama program penghijauan"
onChange={(val) => stateCreate.create.form.name = val.target.value} value={stateCreate.create.form.name || ''}
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Penghijauan</Text> <Text fz="sm" fw="bold" mb={6}>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} /> Ikon Program Penghijauan
</Box> </Text>
<TextInput <SelectIconProgram
onChange={(e) => stateCreate.create.form.judul = e.currentTarget.value} onChange={(value) => (stateCreate.create.form.icon = value)}
label={<Text fw={"bold"} fz={"sm"}>Judul Deskripsi Program Penghijauan</Text>}
placeholder='Masukkan judul deskripsi program penghijauan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Penghijauan</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/> />
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <TextInput
label={<Text fz="sm" fw="bold">Judul Deskripsi Program Penghijauan</Text>}
placeholder="Masukkan judul deskripsi program penghijauan"
value={stateCreate.create.form.judul || ''}
onChange={(e) => (stateCreate.create.form.judul = e.target.value)}
required
/>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi Program Penghijauan
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
<Group justify="right" mt="sm">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,30 +2,55 @@
/* 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 {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDeviceImac, IconHomeEco, IconLeaf, Box,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent, Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDeviceImac,
IconHomeEco,
IconLeaf,
IconRecycle,
IconScale,
IconSearch,
IconShieldFilled,
IconTent,
IconTrashFilled, IconTrashFilled,
IconTrendingUp, IconTrendingUp,
IconTrophy, IconTrophy,
IconTruckFilled IconTruckFilled,
IconPlus,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { 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 programPenghijauanState from '../../_state/lingkungan/program-penghijauan'; import programPenghijauanState from '../../_state/lingkungan/program-penghijauan';
function ProgramPenghijauan() { function ProgramPenghijauan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Program Penghijauan' title='Program Penghijauan'
placeholder='pencarian' placeholder='Cari program penghijauan...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -41,18 +66,10 @@ function ListProgramPenghijauan({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search)
}, [page]) }, [page, search])
const filteredData = (data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.judul.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,
@@ -73,93 +90,104 @@ function ListProgramPenghijauan({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={650} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Program Penghijauan'
href='/admin/lingkungan/program-penghijauan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Penghijauan</TableTh>
<TableTh>Judul Deskripsi Program Penghijauan</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program penghijauan yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList {/* Header Section */}
title='List Program Penghijauan' <Box mb="md" display="flex" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
href='/admin/lingkungan/program-penghijauan/create' <Title order={4}>Daftar Program Penghijauan</Title>
/> <Tooltip label="Tambah Program Penghijauan" withArrow>
<Box style={{ overflowY: 'auto' }}> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/lingkungan/program-penghijauan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Box>
{/* Table Section */}
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> <TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Penghijauan</TableTh> <TableTh style={{ width: '25%' }}>Nama Program</TableTh>
<TableTh style={{ width: '35%' }}>Judul Deskripsi Program Penghijauan</TableTh> <TableTh style={{ width: '35%' }}>Judul / Deskripsi</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh> <TableTh style={{ width: '15%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh> <TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item, index) => (
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTr key={item.id}>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd> <TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}> <TableTd>
<Text lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.judul }}/> <Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '10%' }}> <TableTd>
{iconMap[item.icon] && ( <Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.judul }} />
<Box title={item.icon}> </TableTd>
{React.createElement(iconMap[item.icon], { size: 24 })} <TableTd style={{ textAlign: 'center' }}>
</Box> {iconMap[item.icon] && (
)} <Box title={item.icon} mx="auto">
</TableTd> {React.createElement(iconMap[item.icon], { size: 22 })}
<TableTd style={{ width: '15%', textAlign: 'center' }}> </Box>
<Button onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${item.id}`)}> )}
<IconDeviceImac size={25} /> </TableTd>
</Button> <TableTd style={{ textAlign: 'center' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text c="dimmed">Tidak ada data program penghijauan yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Paper> </Paper>
{/* Pagination */}
<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 ProgramPenghijauan; export default ProgramPenghijauan;

View File

@@ -72,7 +72,8 @@ function Page() {
<Image <Image
pt={{ base: 0, md: 60 }} pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"} src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }} w="100%"
maw={300}
alt="Foto Profil PPID" alt="Foto Profil PPID"
radius="md" radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }} onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}

View File

@@ -117,14 +117,14 @@ function ListPegawaiPPID({ search }: { search: string }) {
).map((item) => ( ).map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap} {item.namaLengkap}
</Text> </Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Box w={150}>
<Badge variant="light" color="blue"> <Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'} {item.posisi?.nama || 'Belum diatur'}
</Badge> </Badge>

View File

@@ -68,10 +68,10 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Posisi Organisasi PPID</Title> <Title order={4}>Daftar Posisi Organisasi PPID</Title>
<Tooltip label="Tambah Posisi Organisasi" withArrow> <Tooltip label="Tambah Posisi Organisasi" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')} onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
> >
Tambah Baru Tambah Baru
@@ -82,51 +82,54 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Nama Posisi</TableTh> <TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh> <TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Hierarki</TableTh> <TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '25%' }}> <TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text> <Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '45%' }}> <TableTd style={{ width: '20%' }}>
<Text lineClamp={2} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} /> <Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text> <Text>{item.hierarki || '-'}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '20%' }}>
<Group gap="xs"> <Tooltip label="Edit" withArrow>
<Tooltip label="Edit" withArrow> <Button
<Button variant="light"
variant="light" color="green"
color="green" size="sm"
size="sm" onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)} >
> <IconEdit size={18} />
<IconEdit size={18} /> </Button>
</Button> </Tooltip>
</Tooltip> </TableTd>
<Tooltip label="Hapus" withArrow> <TableTd style={{ width: '20%' }}>
<Button <Tooltip label="Hapus" withArrow>
variant="light" <Button
color="red" variant="light"
size="sm" color="red"
onClick={() => { size="sm"
setSelectedId(item.id); onClick={() => {
setModalHapus(true); setSelectedId(item.id);
}} setModalHapus(true);
> }}
<IconTrash size={18} /> >
</Button> <IconTrash size={18} />
</Tooltip> </Button>
</Group> </Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))

View File

@@ -11,6 +11,7 @@ import KategoriPotensi from "./potensi/kategori-potensi";
import KategoriBerita from "./berita/kategori-berita"; import KategoriBerita from "./berita/kategori-berita";
import KategoriPengumuman from "./pengumuman/kategori-pengumuman"; import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
import MantanPerbekel from "./profile/profile-mantan-perbekel"; import MantanPerbekel from "./profile/profile-mantan-perbekel";
import AjukanPermohonan from "./layanan/ajukan_permohonan";
const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] }) const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
@@ -26,6 +27,7 @@ const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
.use(KategoriPotensi) .use(KategoriPotensi)
.use(KategoriBerita) .use(KategoriBerita)
.use(KategoriPengumuman) .use(KategoriPengumuman)
.use(AjukanPermohonan)
export default Desa; export default Desa;

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
nik: string;
alamat: string;
nomorKk: string;
kategoriId: string;
}
export default async function createAjukanPermohonan(context: Context){
const body = context.body as FormCreate;
await prisma.ajukanPermohonan.create({
data: {
nama: body.nama,
nik: body.nik,
alamat: body.alamat,
nomorKk: body.nomorKk,
kategoriId: body.kategoriId
}
})
return {
success: true,
message: 'Ajukan permohonan berhasil dibuat',
data: {
...body
}
}
}

View File

@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function deleteAjukanPermohonan(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
message: "ID tidak diberikan",
};
}
await prisma.ajukanPermohonan.delete({
where: { id },
});
return {
status: 200,
message: "Ajukan permohonan berhasil dihapus",
};
}

View File

@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function findManyAjukanPermohonan(context: Context) {
// 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' } },
{ nik: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
{ nomorKk: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.ajukanPermohonan.findMany({
where,
include: {
kategori: true
},
skip,
take: limit,
orderBy: { createdAt: 'asc' },
}),
prisma.ajukanPermohonan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data dengan pagination",
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",
};
}
}
export default findManyAjukanPermohonan;

View File

@@ -0,0 +1,66 @@
import prisma from "@/lib/prisma";
export default async function findUniqueAjukanPermohonan(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json(
{
success: false,
message: "ID tidak boleh kosong",
},
{ status: 400 }
);
}
try {
if (typeof id !== "string") {
return Response.json(
{
success: false,
message: "ID tidak valid",
},
{ status: 400 }
);
}
const data = await prisma.ajukanPermohonan.findUnique({
where: { id },
include: {
kategori: true,
},
});
if (!data) {
return Response.json(
{
success: false,
message: "Ajukan permohonan tidak ditemukan",
},
{ status: 404 }
);
}
return Response.json(
{
success: true,
message: "Success fetch ajukan permohonan by ID",
data,
},
{ status: 200 }
);
} catch (e) {
console.error("Find by ID error:", e);
return Response.json(
{
success: false,
message:
"Gagal mengambil ajukan permohonan: " +
(e instanceof Error ? e.message : "Unknown error"),
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,36 @@
import Elysia, { t } from "elysia";
import createAjukanPermohonan from "./create";
import findManyAjukanPermohonan from "./findMany";
import findUniqueAjukanPermohonan from "./findUnique";
import updateAjukanPermohonan from "./updt";
import deleteAjukanPermohonan from "./del";
const AjukanPermohonan = new Elysia({
prefix: "ajukanpermohonan",
tags: ["Desa/Layanan/AjukanPermohonan"],
})
.get("/findMany", findManyAjukanPermohonan)
.post("/create", createAjukanPermohonan, {
body: t.Object({
nama: t.String(),
nik: t.String(),
alamat: t.String(),
nomorKk: t.String(),
kategoriId: t.String(),
})
})
.get("/:id", findUniqueAjukanPermohonan)
.put("/:id", updateAjukanPermohonan, {
body: t.Object({
nama: t.String(),
nik: t.String(),
alamat: t.String(),
nomorKk: t.String(),
kategoriId: t.String(),
})
})
.delete("/del/:id", deleteAjukanPermohonan)
export default AjukanPermohonan;

View File

@@ -0,0 +1,48 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
id: string;
nama: string;
nik: string;
alamat: string;
nomorKk: string;
kategoriId: string;
};
export default async function updateAjukanPermohonan(context: Context) {
const id = context.params?.id;
const body = context.body as FormUpdate;
if (!id) {
return {
success: false,
message: "ID tidak diberikan",
};
}
const existing = await prisma.ajukanPermohonan.findUnique({
where: { id },
include: {
kategori: true,
},
});
if (!existing) {
return {
success: false,
message: "Ajukan permohonan tidak ditemukan",
};
}
const updated = await prisma.ajukanPermohonan.update({
where: { id },
data: body,
});
return {
success: true,
message: "Success update ajukan permohonan",
data: updated,
};
}

View File

@@ -0,0 +1,27 @@
import prisma from "@/lib/prisma";
export default async function pelayananSuratKeteranganFindManyAll() {
try {
const data = await prisma.pelayananSuratKeterangan.findMany({
where: { isActive: true },
orderBy: { createdAt: "desc" },
include: {
image: true,
image2: true,
},
});
return {
success: true,
message: "Berhasil ambil semua data pelayanan surat keterangan",
data,
};
} catch (e) {
console.error("Error di findManyAll:", e);
return {
success: false,
message: "Gagal mengambil data pelayanan surat keterangan",
data: [],
};
}
}

View File

@@ -4,11 +4,12 @@ import pelayananSuratKeteranganFindUnique from "./findUnique";
import pelayananSuratKeteranganCreate from "./create"; import pelayananSuratKeteranganCreate from "./create";
import pelayananSuratKeteranganUpdate from "./updt"; import pelayananSuratKeteranganUpdate from "./updt";
import pelayananSuratKeteranganDelete from "./del"; import pelayananSuratKeteranganDelete from "./del";
import pelayananSuratKeteranganFindManyAll from "./findManyAll";
import { t } from "elysia"; import { t } from "elysia";
const PelayananSuratKeterangan = new Elysia({ prefix: "/pelayanansuratketerangan", tags: ["Desa/Layanan/Pelayanan Surat Keterangan"] }) const PelayananSuratKeterangan = new Elysia({ prefix: "/pelayanansuratketerangan", tags: ["Desa/Layanan/Pelayanan Surat Keterangan"] })
.get("/find-many", pelayananSuratKeteranganFindMany) .get("/find-many", pelayananSuratKeteranganFindMany)
.get("/findManyAll", pelayananSuratKeteranganFindManyAll)
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await pelayananSuratKeteranganFindUnique(new Request(context.request)); const response = await pelayananSuratKeteranganFindUnique(new Request(context.request));
return response; return response;

View File

@@ -8,7 +8,6 @@ type JadwalKegiatanInput = {
layananJadwalKegiatan: { content: string }; layananJadwalKegiatan: { content: string };
syaratKetentuanJadwalKegiatan: { content: string }; syaratKetentuanJadwalKegiatan: { content: string };
dokumenJadwalKegiatan: { content: string }; dokumenJadwalKegiatan: { content: string };
pendaftaranJadwalKegiatan: { name: string, tanggal: string, namaOrangtua: string, nomor: string, alamat: string, catatan: string };
} }
const jadwalKegiatanCreate = async (context: Context) => { const jadwalKegiatanCreate = async (context: Context) => {
@@ -21,16 +20,14 @@ const jadwalKegiatanCreate = async (context: Context) => {
layananJadwalKegiatan, layananJadwalKegiatan,
syaratKetentuanJadwalKegiatan, syaratKetentuanJadwalKegiatan,
dokumenJadwalKegiatan, dokumenJadwalKegiatan,
pendaftaranJadwalKegiatan,
} = body; } = body;
const [createdInformasiJadwalKegiatan, createdDeskripsiJadwalKegiatan, createdLayananJadwalKegiatan, createdSyaratKetentuanJadwalKegiatan, createdDokumenJadwalKegiatan, createdPendaftaranJadwalKegiatan] = await Promise.all([ const [createdInformasiJadwalKegiatan, createdDeskripsiJadwalKegiatan, createdLayananJadwalKegiatan, createdSyaratKetentuanJadwalKegiatan, createdDokumenJadwalKegiatan] = await Promise.all([
prisma.informasiJadwalKegiatan.create({ data: informasiJadwalKegiatan }), prisma.informasiJadwalKegiatan.create({ data: informasiJadwalKegiatan }),
prisma.deskripsiJadwalKegiatan.create({ data: deskripsiJadwalKegiatan }), prisma.deskripsiJadwalKegiatan.create({ data: deskripsiJadwalKegiatan }),
prisma.layananJadwalKegiatan.create({ data: layananJadwalKegiatan }), prisma.layananJadwalKegiatan.create({ data: layananJadwalKegiatan }),
prisma.syaratKetentuanJadwalKegiatan.create({ data: syaratKetentuanJadwalKegiatan }), prisma.syaratKetentuanJadwalKegiatan.create({ data: syaratKetentuanJadwalKegiatan }),
prisma.dokumenJadwalKegiatan.create({ data: dokumenJadwalKegiatan }), prisma.dokumenJadwalKegiatan.create({ data: dokumenJadwalKegiatan }),
prisma.pendaftaranJadwalKegiatan.create({ data: pendaftaranJadwalKegiatan }),
]) ])
const jadwalKegiatan = await prisma.jadwalKegiatan.create({ const jadwalKegiatan = await prisma.jadwalKegiatan.create({
@@ -41,7 +38,6 @@ const jadwalKegiatanCreate = async (context: Context) => {
layananJadwalKegiatanId: createdLayananJadwalKegiatan.id, layananJadwalKegiatanId: createdLayananJadwalKegiatan.id,
syaratKetentuanJadwalKegiatanId: createdSyaratKetentuanJadwalKegiatan.id, syaratKetentuanJadwalKegiatanId: createdSyaratKetentuanJadwalKegiatan.id,
dokumenJadwalKegiatanId: createdDokumenJadwalKegiatan.id, dokumenJadwalKegiatanId: createdDokumenJadwalKegiatan.id,
pendaftaranJadwalKegiatanId: createdPendaftaranJadwalKegiatan.id,
}, },
include: { include: {
informasijadwalkegiatan: true, informasijadwalkegiatan: true,
@@ -49,7 +45,6 @@ const jadwalKegiatanCreate = async (context: Context) => {
layananjadwalkegiatan: true, layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true, syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true, dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
} }
}) })
return { return {

View File

@@ -19,7 +19,6 @@ const jadwalKegiatanDelete = async (context: Context) => {
layananjadwalkegiatan: true, layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true, syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true, dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
} }
}) })

View File

@@ -20,7 +20,6 @@ export default async function jadwalKegiatanFindMany(context: Context) {
{layananjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } }, {layananjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
{syaratketentuanjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } }, {syaratketentuanjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
{dokumenjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } }, {dokumenjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
{pendaftaranjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
]; ];
} }
try { try {
@@ -33,8 +32,7 @@ export default async function jadwalKegiatanFindMany(context: Context) {
layananjadwalkegiatan: true, layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true, syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true, dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true, },
},
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },

View File

@@ -28,7 +28,6 @@ export default async function jadwalKegiatanFindUnique(request: Request) {
layananjadwalkegiatan: true, layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true, syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true, dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
} }
}) })

View File

@@ -30,14 +30,6 @@ const JadwalKegiatan = new Elysia({
dokumenJadwalKegiatan: t.Object({ dokumenJadwalKegiatan: t.Object({
content: t.String(), content: t.String(),
}), }),
pendaftaranJadwalKegiatan: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
}), }),
}) })
.get("/find-many", jadwalKegiatanFindMany) .get("/find-many", jadwalKegiatanFindMany)
@@ -75,14 +67,6 @@ const JadwalKegiatan = new Elysia({
dokumenJadwalKegiatan: t.Object({ dokumenJadwalKegiatan: t.Object({
content: t.String(), content: t.String(),
}), }),
pendaftaranJadwalKegiatan: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
}), }),
} }
); );

View File

@@ -13,14 +13,6 @@ type JadwalKegiatanUpdateInput = {
layananJadwalKegiatan: { content: string }; layananJadwalKegiatan: { content: string };
syaratKetentuanJadwalKegiatan: { content: string }; syaratKetentuanJadwalKegiatan: { content: string };
dokumenJadwalKegiatan: { content: string }; dokumenJadwalKegiatan: { content: string };
pendaftaranJadwalKegiatan: {
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
};
}; };
const jadwalKegiatanUpdate = async (context: Context) => { const jadwalKegiatanUpdate = async (context: Context) => {
@@ -50,7 +42,6 @@ const jadwalKegiatanUpdate = async (context: Context) => {
layananJadwalKegiatan, layananJadwalKegiatan,
syaratKetentuanJadwalKegiatan, syaratKetentuanJadwalKegiatan,
dokumenJadwalKegiatan, dokumenJadwalKegiatan,
pendaftaranJadwalKegiatan,
} = body; } = body;
await Promise.all([ await Promise.all([
@@ -74,10 +65,6 @@ const jadwalKegiatanUpdate = async (context: Context) => {
where: { id: existing.dokumenJadwalKegiatanId }, where: { id: existing.dokumenJadwalKegiatanId },
data: dokumenJadwalKegiatan data: dokumenJadwalKegiatan
}), }),
prisma.pendaftaranJadwalKegiatan.update({
where: { id: existing.pendaftaranJadwalKegiatanId },
data: pendaftaranJadwalKegiatan,
}),
]); ]);
const updated = await prisma.jadwalKegiatan.update({ const updated = await prisma.jadwalKegiatan.update({
@@ -91,7 +78,6 @@ const jadwalKegiatanUpdate = async (context: Context) => {
layananjadwalkegiatan: true, layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true, syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true, dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
}, },
}); });
return { return {

View File

@@ -1,15 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriKegiatanFindMany() { export default async function kategoriKegiatanFindMany(context: Context) {
const data = await prisma.kategoriKegiatan.findMany(); const page = Number(context.query.page) || 1;
return { const limit = Number(context.query.limit) || 10;
success: true, const search = (context.query.search as string) || "";
data: data.map((item: any) => { const skip = (page - 1) * limit;
return {
id: item.id, // Buat where clause
nama: item.nama, const where: any = { isActive: true };
}
}), // Tambahkan pencarian (jika ada)
if (search) {
where.OR = [{ nama: { contains: search, mode: "insensitive" } }];
}
try {
const [data, total] = await Promise.all([
prisma.kategoriKegiatan.findMany({
where: where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.kategoriKegiatan.count({
where: where,
}),
]);
return {
success: true,
data: data.map((item: any) => {
return {
id: item.id,
nama: item.nama,
};
}),
message: "Success fetch administrasi online with pagination",
page,
limit,
totalPages: Math.ceil(total / limit),
total,
}; };
} } catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch administrasi online with pagination",
};
}
}

View File

@@ -24,7 +24,7 @@ export default async function pengelolaanSampahFindMany(context: Context) {
where, where,
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'asc' },
}), }),
prisma.pengelolaanSampah.count({ prisma.pengelolaanSampah.count({
where, where,

View File

@@ -1,105 +1,83 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Image, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; import { Stack, Box, Container, Grid, GridCol, Group, Paper, TextInput, Text, Image, Flex, Button } from '@mantine/core';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto'; import { IconCalendar, IconMapPin, IconSearch, IconUsersGroup } from '@tabler/icons-react';
import React from 'react';
import Link from 'next/link';
const data1 = [
{
id: 1,
judul: 'Peran Pecalang dalam Keamanan Desa',
image: '/api/img/pecalang.png',
pengertian: 'Pecalang adalah petugas keamanan adat di Bali yang memiliki peran penting dalam menjaga ketertiban dan budaya lokal. Tugas mereka meliputi:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Mengamankan upacara adat dan kegiatan keagamaan.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Mengatur lalu lintas saat ada perayaan atau kegiatan besar.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Berpatroli untuk mencegah gangguan keamanan di lingkungan desa.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Berkoordinasi dengan aparat desa dan kepolisian dalam penanganan situasi darurat.</ListItem>
</List>
},
{
id: 2,
judul: 'Patwal (Patroli Pengawal) Desa',
image: '/api/img/patwal-1.png',
pengertian: 'Selain Pecalang, Desa Darmasaba juga memiliki Patwal yang bertugas menjaga keamanan sehari-hari. Peran mereka antara lain:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Berpatroli secara rutin untuk memastikan lingkungan tetap aman.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Menjaga ketertiban lalu lintas di area desa.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Melakukan tindakan preventif terhadap potensi gangguan keamanan.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Siap siaga dalam keadaan darurat untuk membantu warga.</ListItem>
</List>
},
{
id: 3,
judul: 'Layanan Keamanan yang Tersedia',
image: '/api/img/pospecalang.png',
pengertian: 'Jika terjadi keadaan darurat atau membutuhkan bantuan keamanan, warga dapat menghubungi:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Pos Pecalang Desa: [Masukkan Nomor Kontak].</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Patwal Desa Darmasaba: [Masukkan Nomor Kontak].</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Polsek Terdekat: 110 (Layanan Kepolisian).</ListItem>
</List>
},
{
id: 4,
judul: 'Program Keamanan Desa',
image: '/api/img/rond.png',
pengertian: 'Untuk meningkatkan keamanan, Desa Darmasaba menjalankan berbagai program, seperti:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Ronda Malam Warga: Kegiatan jaga malam secara bergilir oleh warga bersama Pecalang dan Patwal.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Sosialisasi Keamanan: Edukasi bagi warga tentang cara menjaga keamanan lingkungan.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}> Pengawasan Kamera CCTV: Memantau titik- titik strategis untuk mencegah tindak kejahatan.</ListItem>
</List>
}
]
function Page() { function Page() {
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
<Box px={{ base: 'md', md: 100 }}> {/* Header */}
<BackButton /> <Container size="lg" px="md">
</Box> <Stack align="center" gap={0} mb="xl">
<Box> <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> Program Gotong Royong
Keamanan Lingkungan (Pecalang / Patwal) </Text>
</Text> <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
<Text px={{ base: 20, md: 150 }} ta={"center"} fz={{ base: "h4", md: "h3" }} > Desa Darmasaba
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. </Text>
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<SimpleGrid
pb={10}
cols={{
base: 1,
md: 3,
}}>
{data1.map((v, k) => {
return (
<Paper radius={10} key={k} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}>
<Center px={10} py={20}>
<Image src={v.image} alt='' />
</Center>
<Box px={'lg'}>
<Box pb={20}>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.judul}
</Text>
<Text pb={10} fz={"h4"} ta={'justify'}>
{v.pengertian}
</Text>
<Box px={10}>
{v.deskripsi}
</Box>
</Box>
</Box>
</Stack>
</Paper>
)
})}
</SimpleGrid>
</Stack> </Stack>
</Container>
{/* Tabs Menu */}
<Box px={{ base: "md", md: "xl" }} py="md" bg={colors['BG-trans']} mb="md">
<Grid align="center" justify="space-between" mb={20}>
<GridCol span={{ base: 12, md: 8 }}>
<Group gap="md" wrap="wrap">
<Paper bg={colors['blue-button']} radius="xl" py={5} px={20}>
<Text c={colors['white-1']} size="sm">
Semua
</Text>
</Paper>
{['Kebersihan', 'Infrastruktur', 'Sosial', 'Lingkungan'].map((kategori) => (
<Paper key={kategori} bg={colors['blue-button-trans']} radius="xl" py={5} px={20}>
<Text size="sm">
{kategori}
</Text>
</Paper>
))}
</Group>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<TextInput
radius="lg"
placeholder="Cari Program Gotong Royong"
leftSection={<IconSearch size={18} />}
w="100%"
/>
</GridCol>
</Grid>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Stack gap={'xs'}>
<Image radius={20} src={'/api/img/gotong-royong.png'} w={'100%'} alt='' />
<Text fw={"bold"} fz={{ base: "h2", md: "h1" }}>Membangun Fasilitas Desa</Text>
<Group>
<Paper py={5} px={20} bg={colors['blue-button-trans']} radius={20}>
<Text c={colors['white-1']}>Sosial</Text>
</Paper>
</Group>
<Text fz={{ base: "h4", md: "h3" }}>
Program Pembangunan Fasilitas Desa Maju, Masyarakat Sejahtera.
</Text>
<Flex gap={5} align={'center'}>
<IconCalendar color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>1 April 2025</Text>
</Flex>
<Flex gap={5} align={'center'}>
<IconMapPin color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>Banjar Desa Darmasaba</Text>
</Flex>
<Flex gap={5} align={'center'}>
<IconUsersGroup color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>30 Partisipan</Text>
</Flex>
<Text fw={'bold'} fz={'md'}>Deskripsi : Program pembangunan Pura sebagai pusat spiritual dan budaya desa, melibatkan gotong royong masyarakat dalam pembangunan struktur utama serta ornamen tradisional.</Text>
<Group py={20} justify='center'>
<Button component={Link} href={'https://www.whatsapp.com/?lang=id'} bg={colors['blue-button']} >Daftar Sebagai Relawan</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
</Stack> </Stack>
); );

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, ScrollArea, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core'; import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -23,95 +23,52 @@ function LayoutTabsBerita({
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
// Get active tab from URL path
const activeTab = pathname.split('/').pop() || 'semua'; const activeTab = pathname.split('/').pop() || 'semua';
// Get initial search value from URL
const initialSearch = searchParams.get('search') || ''; const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch); const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null); const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Update active tab state when pathname changes
const [activeTabState, setActiveTabState] = useState(activeTab); const [activeTabState, setActiveTabState] = useState(activeTab);
useEffect(() => { useEffect(() => {
setActiveTabState(activeTab); setActiveTabState(activeTab);
}, [activeTab]); }, [activeTab]);
// Clean up timeouts on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
if (searchTimeout !== null) { if (searchTimeout !== null) clearTimeout(searchTimeout);
clearTimeout(searchTimeout);
}
}; };
}, [searchTimeout]); }, [searchTimeout]);
// Handle search input change with debounce
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value; const value = event.target.value;
setSearchValue(value); setSearchValue(value);
// Clear previous timeout if (searchTimeout !== null) clearTimeout(searchTimeout);
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
// Set new timeout
const newTimeout = window.setTimeout(() => { const newTimeout = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
if (value) { if (value) params.set('search', value);
params.set('search', value); else params.delete('search');
} else {
params.delete('search');
}
// Only update URL if the search value has actually changed
if (params.toString() !== searchParams.toString()) { if (params.toString() !== searchParams.toString()) {
router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`); router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
} }
}, 500); // 500ms debounce delay }, 500);
setSearchTimeout(newTimeout); setSearchTimeout(newTimeout);
}; };
const tabs = [
{
label: "Semua",
value: "semua",
href: "/darmasaba/desa/berita/semua"
},
{
label: "Budaya",
value: "budaya",
href: "/darmasaba/desa/berita/budaya"
},
{
label: "Pemerintahan",
value: "pemerintahan",
href: "/darmasaba/desa/berita/pemerintahan"
},
{
label: "Ekonomi",
value: "ekonomi",
href: "/darmasaba/desa/berita/ekonomi"
},
{
label: "Pembangunan",
value: "pembangunan",
href: "/darmasaba/desa/berita/pembangunan"
},
{
label: "Sosial",
value: "sosial",
href: "/darmasaba/desa/berita/sosial"
},
{
label: "Teknologi",
value: "teknologi",
href: "/darmasaba/desa/berita/teknologi"
},
const tabs = [
{ label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
{ label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
{ label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
{ label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
{ label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
{ label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
{ label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
]; ];
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
if (!value) return; if (!value) return;
const tab = tabs.find(t => t.value === value); const tab = tabs.find(t => t.value === value);
@@ -127,16 +84,29 @@ function LayoutTabsBerita({
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" > <Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center"> <Group justify='space-between' align="center">
Portal Berita Darmasaba <Stack gap="0">
</Text> <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
<Text ta="center" px="md"> Portal Berita Darmasaba
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba </Text>
</Text> <Text>
</Stack> Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Container> </Text>
</Stack>
<Box>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</Box>
</Group>
</Box>
<Tabs <Tabs
color={colors['blue-button']} color={colors['blue-button']}
@@ -145,33 +115,25 @@ function LayoutTabsBerita({
onChange={handleTabChange} onChange={handleTabChange}
> >
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}> <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid> {/* SCROLLABLE TABS */}
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}> <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
<ScrollArea type="auto" offsetScrollbars> <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
<TabsList> {tabs.map((tab, index) => (
{tabs.map((tab, index) => ( <TabsTab
<TabsTab key={index}
key={index} value={tab.value}
value={tab.value} onClick={() => router.push(tab.href)}
onClick={() => router.push(tab.href)} style={{
> flex: '0 0 auto', // Prevent shrinking
{tab.label} minWidth: 100, // optional: makes them touch-friendly
</TabsTab> textAlign: 'center'
))} }}
</TabsList> >
</ScrollArea> {tab.label}
</GridCol> </TabsTab>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}> ))}
<TextInput </TabsList>
radius="lg" </Box>
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</GridCol>
</Grid>
</Box> </Box>
{children} {children}
@@ -180,4 +142,4 @@ function LayoutTabsBerita({
); );
} }
export default LayoutTabsBerita; export default LayoutTabsBerita;

View File

@@ -1,12 +1,12 @@
'use client'; 'use client';
import { useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import BackButton from '../../layanan/_com/BackButto';
import dynamic from 'next/dynamic';
import type { SearchBarProps } from './searchBar';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Container, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import dynamic from 'next/dynamic';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import BackButton from '../../layanan/_com/BackButto';
import type { SearchBarProps } from './searchBar';
// Define tabs outside the component to ensure consistency between server and client // Define tabs outside the component to ensure consistency between server and client
const TABS = [ const TABS = [
@@ -35,7 +35,7 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
// Set default active tab to empty string to prevent hydration mismatch // Set default active tab to empty string to prevent hydration mismatch
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
@@ -47,7 +47,7 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
// Update active tab based on current route - only on client side // Update active tab based on current route - only on client side
useEffect(() => { useEffect(() => {
if (!isClient) return; if (!isClient) return;
const currentTab = TABS.find(tab => pathname.includes(tab.value)); const currentTab = TABS.find(tab => pathname.includes(tab.value));
if (currentTab) { if (currentTab) {
setActiveTab(currentTab.value); setActiveTab(currentTab.value);
@@ -75,42 +75,38 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Container size="lg" px="md"> <Box px={{ base: "md", md: 100 }}>
<Stack align="center" gap="0"> <Stack align="center" gap="0">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center"> <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Galeri Kegiatan Desa Darmasaba Galeri Kegiatan Desa Darmasaba
</Text> </Text>
</Stack> </Stack>
</Container> <Box>
<SearchBar />
</Box>
</Box>
<Tabs <Tabs
value={isClient ? activeTab : undefined} value={isClient ? activeTab : undefined}
defaultValue={TABS[0].value} defaultValue={TABS[0].value}
onChange={handleTabChange} onChange={handleTabChange}
color={colors['blue-button']} color={colors['blue-button']}
variant="pills" variant="pills"
keepMounted={false} keepMounted={false}
> >
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}> <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid> <TabsList>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}> {TABS.map((tab) => (
<TabsList> <TabsTab
{TABS.map((tab) => ( key={tab.value}
<TabsTab value={tab.value}
key={tab.value} component="button"
value={tab.value} type="button"
component="button" >
type="button" {tab.label}
> </TabsTab>
{tab.label} ))}
</TabsTab> </TabsList>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<SearchBar />
</GridCol>
</Grid>
</Box> </Box>
<Container size={'xl'}> <Container size={'xl'}>

View File

@@ -2,7 +2,7 @@
'use client'; 'use client';
import { TextInput } from '@mantine/core'; import { Group, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
@@ -65,13 +65,15 @@ export function SearchBar({
}, []); }, []);
return ( return (
<TextInput <Group justify='center'>
<TextInput
radius="lg" radius="lg"
placeholder={placeholder} placeholder={placeholder}
leftSection={searchIcon} leftSection={searchIcon}
w="100%" w={{ base: '100%', md: '50%' }}
value={searchValue} value={searchValue}
onChange={handleSearchChange} onChange={handleSearchChange}
/> />
</Group>
); );
} }

View File

@@ -2,19 +2,24 @@
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, Center, Pagination, Paper, SimpleGrid, Spoiler, Stack, Text } from '@mantine/core'; import {
Box,
Center,
Pagination,
Paper,
SimpleGrid,
Spoiler,
Stack,
Text,
} from '@mantine/core';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
export default function VideoContent() { export default function VideoContent() {
const [expanded, setExpanded] = useState(false); // ✅ expanded state per index
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
const videoState = useSnapshot(stateGallery.video); const videoState = useSnapshot(stateGallery.video);
const { const { data, page, totalPages, loading } = videoState.findMany;
data,
page,
totalPages,
loading,
} = videoState.findMany;
// Handle search and pagination changes // Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => { const loadData = useCallback((pageNum: number, searchTerm: string) => {
@@ -27,24 +32,18 @@ export default function VideoContent() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || ''; const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1'); const urlPage = parseInt(urlParams.get('page') || '1');
loadData(urlPage, urlSearch); loadData(urlPage, urlSearch);
}; };
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => { const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail; const { search } = (e as CustomEvent).detail;
loadData(1, search); loadData(1, search);
}; };
// Initial load
handleRouteChange(); handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange); window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener); window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => { return () => {
window.removeEventListener('popstate', handleRouteChange); window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener); window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
@@ -57,6 +56,13 @@ export default function VideoContent() {
loadData(newPage, search); loadData(newPage, search);
}; };
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
const dataVideo = data || []; const dataVideo = data || [];
if (loading && !data) { if (loading && !data) {
@@ -72,7 +78,13 @@ export default function VideoContent() {
<SimpleGrid cols={{ base: 1, md: 3 }}> <SimpleGrid cols={{ base: 1, md: 3 }}>
{dataVideo.map((v, k) => ( {dataVideo.map((v, k) => (
<Box key={k}> <Box key={k}>
<Paper mb={50} p="md" radius={26} bg={colors['white-trans-1']} w={{ base: '100%', md: '100%' }}> <Paper
mb={50}
p="md"
radius={26}
bg={colors['white-trans-1']}
w={{ base: '100%', md: '100%' }}
>
<Box> <Box>
<Center> <Center>
<Box <Box
@@ -109,8 +121,8 @@ export default function VideoContent() {
Hide details Hide details
</Text> </Text>
} }
expanded={expanded} expanded={expandedMap[k] || false}
onExpandedChange={setExpanded} onExpandedChange={(val) => toggleExpanded(k, val)}
> >
<Text <Text
ta="justify" ta="justify"
@@ -137,15 +149,15 @@ export default function VideoContent() {
); );
} }
// ✅ Fix: HAPUS SPASI BERLEBIH DI URL // ✅ Fix: convert YouTube URL ke embed
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}`; // ✅ tanpa spasi! 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);
return youtubeUrl; return youtubeUrl;
} }
} }

View File

@@ -2,11 +2,12 @@
'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, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Center, Container, Group, Image, Modal, Paper, Select, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../_com/BackButto'; import BackButton from '../_com/BackButto';
import { useDisclosure } from '@mantine/hooks';
interface LayananData { interface LayananData {
id: string; id: string;
@@ -31,7 +32,12 @@ function Page() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [data, setData] = useState<LayananData | null>(null); const [data, setData] = useState<LayananData | null>(null);
const [opened, { open, close }] = useDisclosure(false);
const stateCreate = useProxy(stateLayananDesa.ajukanPermohonan);
useEffect(() => { useEffect(() => {
state.suratKeterangan.findManyAll.load()
const loadData = async () => { const loadData = async () => {
if (!id) return; if (!id) return;
try { try {
@@ -48,6 +54,22 @@ function Page() {
loadData(); loadData();
}, [id]); }, [id]);
const resetForm = () => {
stateCreate.create.form = {
nama: '',
nik: '',
alamat: '',
nomorKk: '',
kategoriId: '',
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
close();
}
if (loading) { if (loading) {
return ( return (
<Center h="100vh" bg={colors.Bg}> <Center h="100vh" bg={colors.Bg}>
@@ -105,12 +127,76 @@ function Page() {
size="lg" size="lg"
variant="gradient" variant="gradient"
gradient={{ from: '#1C6EA4', to: '#63B3ED' }} gradient={{ from: '#1C6EA4', to: '#63B3ED' }}
onClick={open}
> >
Ajukan Permohonan Ajukan Permohonan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box> </Box>
<Modal
opened={opened}
onClose={close}
radius={0}
transitionProps={{ transition: 'fade', duration: 200 }}
>
<Paper p="md" withBorder>
<Stack gap="xs">
<Title order={3}>Ajukan Permohonan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">NIK</Text>}
placeholder="masukkan NIK"
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
placeholder="masukkan Nomor KK"
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={stateCreate.create.form.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateCreate.create.form.kategoriId = selected.id;
}
} else {
stateCreate.create.form.kategoriId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>
</Stack>
</Paper>
</Modal>
</Stack> </Stack>
); );
} }

View File

@@ -22,7 +22,7 @@ export default function Page() {
<Container w={{ base: "100%", md: "50%" }} > <Container w={{ base: "100%", md: "50%" }} >
<Stack align="center" gap={0}> <Stack align="center" gap={0}>
{/* Bagian Layanan */} {/* Bagian Layanan */}
<Text fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: "1.8rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Layanan Desa Darmasaba Layanan Desa Darmasaba
</Text> </Text>
<Text <Text

View File

@@ -1,7 +1,7 @@
'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, Container, Flex, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -31,7 +31,7 @@ function Page() {
</Box> </Box>
<Container size="lg" px="md"> <Container size="lg" px="md">
<Stack gap="xs" > <Stack gap="xs" >
<Flex justify={"space-between"} align={"center"}> <Group justify={"space-between"} align={"center"}>
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" > <Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
{detail.data?.judul} {detail.data?.judul}
</Text> </Text>
@@ -40,7 +40,7 @@ function Page() {
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text> <Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper> </Paper>
</Group> </Group>
</Flex> </Group>
<Paper bg={colors["white-1"]} p="md"> <Paper bg={colors["white-1"]} p="md">
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: detail.data?.content }} /> <Text fz={"md"} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text fz={"md"} c={colors["blue-button"]} fw="bold" > <Text fz={"md"} c={colors["blue-button"]} fw="bold" >

View File

@@ -1,67 +1,76 @@
'use client' 'use client'
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi'; import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Group, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import {
Box,
Center,
Grid,
GridCol,
Group,
Image,
Pagination,
Paper,
Select,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const state = useProxy(kolaborasiInovasiState) const kolabState = useProxy(kolaborasiInovasiState)
const mitraState = useProxy(mitraKolaborasi)
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<string | null>(null); const [selectedYear, setSelectedYear] = useState<string | null>(null);
// Get unique years from the data // Get unique years from kolaborasiInovasi data
const years = Array.from( const years = Array.from(
new Set( new Set(
state.findMany.data?.map(item => kolabState.findMany.data?.map(item =>
new Date(item.createdAt).getFullYear().toString() new Date(item.createdAt).getFullYear().toString()
) || [] ) || []
) )
) )
.sort((a, b) => b.localeCompare(a)) // Sort descending (newest first) .sort((a, b) => b.localeCompare(a))
.map(year => ({ .map(year => ({ value: year, label: year }));
value: year,
label: year,
}));
const { const { data, page, totalPages, loading, load } = kolabState.findMany;
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search, selectedYear || '') mitraState.findMany.load(page, 10);
}, [page, search, selectedYear]) load(page, 10, search, selectedYear || '');
}, [page, search, selectedYear]);
const mitraData = mitraState.findMany.data || [];
const mitraLoading = mitraState.findMany.loading;
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
return ( return (
<> <>
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
{/* Header Kolaborasi Inovasi */}
<Box px={{ base: 'md', md: 100 }} > <Box px={{ base: 'md', md: 100 }} >
<Grid align='center'> <Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}> <GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Kolaborasi Inovasi Kolaborasi Inovasi
</Text> </Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 3 }}> <GridCol span={{ base: 12, md: 3 }}>
<TextInput <TextInput
radius={"lg"} radius="lg"
placeholder='Cari Kolaborasi Inovasi' placeholder='Cari Kolaborasi Inovasi'
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
@@ -71,40 +80,43 @@ function Page() {
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
{/* Filter Tahun */}
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap="lg" justify="center">
<Group justify='flex-end'> <Group justify="flex-end">
<Select <Select
value={selectedYear} value={selectedYear}
onChange={setSelectedYear} onChange={setSelectedYear}
label={<Text fw={"bold"} fz={"sm"}>Tahun</Text>} label={<Text fw="bold" fz="sm">Tahun</Text>}
placeholder='Semua Tahun' placeholder='Semua Tahun'
clearable clearable
data={[ data={[{ value: '', label: 'Semua Tahun' }, ...years]}
{ value: '', label: 'Semua Tahun' },
...years
]}
/> />
</Group> </Group>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing={'lg'}>
{data.map((v, k) => { {/* List Kolaborasi Inovasi */}
return ( {loading || !data ? (
<Paper p={'xl'} key={k}> <Stack py={10}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text> <Skeleton height={650} />
<Box pr={'lg'} pb={20}> </Stack>
{v.slug} ) : (
</Box> <SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
<Box pr={'lg'}> {data.map((v, k) => (
{v.kolaborator} <Paper p="xl" key={k}>
</Box> <Text fz="h3" fw="bold" c={colors['blue-button']}>{v.name}</Text>
<Box pr="lg" pb={20}>{v.slug}</Box>
<Box pr="lg">{v.kolaborator}</Box>
</Paper> </Paper>
); ))}
})} </SimpleGrid>
</SimpleGrid> )}
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => load(newPage)}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
@@ -113,17 +125,44 @@ function Page() {
</Stack> </Stack>
</Box> </Box>
</Stack> </Stack>
{/* Mitra Kolaborasi Section */}
<Box py={40} px={{ base: "md", md: 100 }} bg={colors['white-trans-1']}> <Box py={40} px={{ base: "md", md: 100 }} bg={colors['white-trans-1']}>
<Stack gap={'lg'} justify='center'> <Stack gap="lg" justify="center">
<Box px={{ base: 'md', md: 100 }} > <Box px={{ base: 'md', md: 100 }}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Mitra Kolaborasi Mitra Kolaborasi
</Text> </Text>
<Text ta={'center'} fz={'h4'}>Kami berkolaborasi dengan berbagai mitra dari berbagai sektor untuk mewujudkan visi Smart Village Darmasaba.</Text> <Text ta="center" fz="h4">
Kami berkolaborasi dengan berbagai mitra dari berbagai sektor untuk mewujudkan visi Smart Village Darmasaba.
</Text>
</Box> </Box>
<Center>
<Image src={'/api/img/logoukm-kolaborasiinvoasi.png'} alt='' w={{ base: 500, md: 650 }} loading="lazy"/> {mitraLoading ? (
</Center> <Center py={20}><Skeleton height={100} width="80%" /></Center>
) : (
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="xl">
{mitraData.map((m) => (
<Paper key={m.id} p="md" shadow="sm" radius="md">
<Center mb="sm">
{m.image?.link ? (
<Image
src={`${process.env.NEXT_PUBLIC_BASE_URL || ''}${m.image.link}`}
alt={m.name}
w={150}
h={100}
fit="cover"
radius="md"
/>
) : (
<Box w={100} h={100} bg={colors['blue-button']} style={{ borderRadius: '50%' }} />
)}
</Center>
<Text ta="center" fw="bold">{m.name}</Text>
</Paper>
))}
</SimpleGrid>
)}
</Stack> </Stack>
</Box> </Box>
</> </>

Some files were not shown because too many files have changed in this diff Show More