fix tampilan admin menu inovasi, sisa menu lingkungan

This commit is contained in:
2025-09-22 10:53:48 +08:00
parent 8e25c91e85
commit 0fc47c28ff
52 changed files with 3787 additions and 1723 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -61,10 +62,37 @@ const ajukanIdeInovatifState = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get();
if (res.status === 200) {
ajukanIdeInovatifState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
ajukanIdeInovatifState.findMany.loading = true; // ✅ Akses langsung via nama path
ajukanIdeInovatifState.findMany.page = page;
ajukanIdeInovatifState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.ajukanideinovatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
ajukanIdeInovatifState.findMany.data = res.data.data ?? [];
ajukanIdeInovatifState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch ajukan ide inovatif paginated:", err);
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
} finally {
ajukanIdeInovatifState.findMany.loading = false;
}
},
},
@@ -97,16 +125,21 @@ const ajukanIdeInovatifState = proxy({
try {
ajukanIdeInovatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/ajukanideinovatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/inovasi/ajukanideinovatif/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan Ide Inovatif berhasil dihapus");
toast.success(
result.message || "Ajukan Ide Inovatif berhasil dihapus"
);
await ajukanIdeInovatifState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");

View File

@@ -64,8 +64,8 @@ const administrasiOnline = proxy({
page: 1,
totalPages: 1,
loading: false,
search: '',
async load(page = 1, limit = 10, search = '') {
search: "",
async load(page = 1, limit = 10, search = "") {
administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page;
administrasiOnline.findMany.search = search;
@@ -215,14 +215,14 @@ const jenisLayanan = proxy({
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get({ query });
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisLayanan.findMany.data = res.data.data ?? [];
jenisLayanan.findMany.totalPages =
res.data.totalPages ?? 1;
jenisLayanan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisLayanan.findMany.data = [];
jenisLayanan.findMany.totalPages = 1;
@@ -494,27 +494,32 @@ const pengaduanMasyarakat = proxy({
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
pengaduanMasyarakat.findMany.loading = true;
search: "",
load: async (page = 1, limit = 10, search = "") => {
pengaduanMasyarakat.findMany.loading = true; // ✅ Akses langsung via nama path
pengaduanMasyarakat.findMany.page = page;
pengaduanMasyarakat.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many"
].get({
query: {
page,
limit,
},
});
].get({ query });
if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err);
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} finally {
pengaduanMasyarakat.findMany.loading = false;
}
@@ -634,13 +639,37 @@ const jenisPengaduan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get();
if (res.status === 200) {
jenisPengaduan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jenisPengaduan.findMany.loading = true; // ✅ Akses langsung via nama path
jenisPengaduan.findMany.page = page;
jenisPengaduan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisPengaduan.findMany.data = res.data.data ?? [];
jenisPengaduan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jenis pengaduan paginated:", err);
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
} finally {
jenisPengaduan.findMany.loading = false;
}
},
},

View File

@@ -54,34 +54,32 @@ const programKreatifState = proxy({
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
programKreatifState.findMany.loading = true; // Use the full path to access the property
search: "",
load: async (page = 1, limit = 10, search = "") => {
programKreatifState.findMany.loading = true; // ✅ Akses langsung via nama path
programKreatifState.findMany.page = page;
programKreatifState.findMany.search = search;
try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({
query: { page, limit },
});
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.inovasi.programkreatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || [];
programKreatifState.findMany.total = res.data.total || 0;
programKreatifState.findMany.totalPages = res.data.totalPages || 1;
programKreatifState.findMany.data = res.data.data ?? [];
programKreatifState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
} catch (err) {
console.error("Gagal fetch program kreatif paginated:", err);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
} finally {
programKreatifState.findMany.loading = false;

View File

@@ -0,0 +1,290 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = 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 = {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
};
const pendaftaranJadwalKegiatanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n");
toast.error(errMsg);
return null;
}
try {
this.loading = true;
const payload = { ...this.form };
const res = await (ApiFetch.api.kesehatan as any)[
"pendaftaran-jadwal-kegiatan"
].create.post(payload);
if (res.status === 200) {
toast.success("Berhasil menambahkan jadwal kegiatan");
this.resetForm();
await pendaftaranJadwalKegiatanState.findMany.load();
return res.data;
}
} catch (err: any) {
const msg = err?.message || "Terjadi kesalahan saat mengirim data";
toast.error(msg);
console.error("SUBMIT ERROR:", err);
return null;
} finally {
this.loading = false;
}
},
resetForm() {
this.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
pendaftaranJadwalKegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
pendaftaranJadwalKegiatanState.findMany.page = page;
pendaftaranJadwalKegiatanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["pendaftaran-jadwal-kegiatan"][
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pendaftaranJadwalKegiatanState.findMany.data = res.data.data ?? [];
pendaftaranJadwalKegiatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
}
} catch (err) {
console.error(
"Gagal fetch pendaftaran jadwal kegiatan paginated:",
err
);
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
} finally {
pendaftaranJadwalKegiatanState.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`
);
if (res.ok) {
const data = await res.json();
pendaftaranJadwalKegiatanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pendaftaranJadwalKegiatanState.delete.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pendaftaran jadwal kegiatan berhasil dihapus"
);
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pendaftaran jadwal kegiatan"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error(
"Terjadi kesalahan saat menghapus pendaftaran jadwal kegiatan"
);
} finally {
pendaftaranJadwalKegiatanState.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/pendaftaran-jadwal-kegiatan/${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 = {
name: data.name,
tanggal: data.tanggal,
namaOrangtua: data.namaOrangtua,
nomor: data.nomor,
alamat: data.alamat,
catatan: data.catatan,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(pendaftaranJadwalKegiatanState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pendaftaranJadwalKegiatanState.edit.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
tanggal: this.form.tanggal,
namaOrangtua: this.form.namaOrangtua,
nomor: this.form.nomor,
alamat: this.form.alamat,
catatan: this.form.catatan,
}),
}
);
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("Berhasil update pendaftaran jadwal kegiatan");
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pendaftaran jadwal kegiatan");
}
} catch (error) {
console.error("Error updating pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pendaftaran jadwal kegiatan"
);
return false;
} finally {
pendaftaranJadwalKegiatanState.edit.loading = false;
}
},
reset() {
pendaftaranJadwalKegiatanState.edit.id = "";
pendaftaranJadwalKegiatanState.edit.form = { ...defaultForm };
},
},
});
export default pendaftaranJadwalKegiatanState;

View File

@@ -1,8 +1,8 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,87 +10,114 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif';
function DetailAjukanIdeInofativDesa() {
const state = useProxy(ajukanIdeInovatifState)
const state = useProxy(ajukanIdeInovatifState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
state.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/ajukan-ide-inovatif")
state.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/ajukan-ide-inovatif");
}
}
};
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = state.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex justify="space-between" gap={"xs"}>
<Text fz={"xl"} fw={"bold"}>Detail Ajukan Ide Inovatif Desa</Text>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "80%", lg: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Header */}
<Flex justify="space-between" align="center">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Ajukan Ide Inovatif Desa
</Text>
<Tooltip label="Hapus Ide Inovatif" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
}}
variant="light"
radius="md"
size="md"
disabled={state.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
</Flex>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.alamat }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Ide Inovatif</Text>
<Text fz={"lg"}>{state.findUnique.data?.namaIde}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Masalah</Text>
<Text fz={"lg"}>{state.findUnique.data?.masalah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Benefit</Text>
<Text fz={"lg"}>{state.findUnique.data?.benefit}</Text>
</Box>
</Stack>
</Paper>
) : null}
{/* Detail Data */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.alamat || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Nama Ide Inovatif</Text>
<Text fz="md" c="dimmed">{data?.namaIde || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Masalah</Text>
<Text fz="md" c="dimmed">{data?.masalah || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Benefit</Text>
<Text fz="md" c="dimmed">{data?.benefit || '-'}</Text>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -99,7 +126,7 @@ function DetailAjukanIdeInofativDesa() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus ajukan ide inovatif ini?'
text="Apakah anda yakin ingin menghapus ajukan ide inovatif ini?"
/>
</Box>
);

View File

@@ -1,8 +1,26 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -15,7 +33,7 @@ function AjukanIdeInovatif() {
<Box>
<HeaderSearch
title='Ajukan Ide Inovatif'
placeholder='pencarian'
placeholder='Cari ide inovatif...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,62 +46,110 @@ function AjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState)
const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
state.findMany.load()
}, [])
load(page, 10, search)
}, [page, search])
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaIde.toLowerCase().includes(keyword) ||
item.masalah.toLowerCase().includes(keyword) ||
item.benefit.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (!state.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title mb={10} order={3}>List Ajukan Ide Inovatif</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Ide Inovatif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.alamat }} />
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.namaIde }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Ide Inovatif</Title>
<Tooltip label="Ajukan Ide Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Nama</TableTh>
<TableTh style={{ width: '30%' }}>Alamat</TableTh>
<TableTh style={{ width: '30%' }}>Nama Ide Inovatif</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.alamat }} />
</TableTd>
<TableTd>
<Text truncate fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.namaIde }} />
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada ide inovatif yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</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>
);
}

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -13,22 +24,23 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const stateInfoTekno = useProxy(infoTeknoState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateInfoTekno.findUnique.data?.name || '',
deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '',
imageId: stateInfoTekno.findUnique.data?.imageId || '',
})
});
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
if (!id) return;
const id = params?.id as string;
if (!id) return;
const loadPenghargaan = async () => {
try {
const data = await stateInfoTekno.edit.load(id);
if (data) {
@@ -38,13 +50,11 @@ function EditInfoTeknologiTepatGuna() {
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error("Error loading info teknologi tepat guna:", error);
toast.error("Gagal memuat data info teknologi tepat guna");
console.error('Error loading info teknologi tepat guna:', error);
toast.error('Gagal memuat data info teknologi tepat guna');
}
};
@@ -55,104 +65,127 @@ function EditInfoTeknologiTepatGuna() {
try {
stateInfoTekno.edit.form = {
...stateInfoTekno.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
...formData,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal upload gambar');
}
stateInfoTekno.edit.form.imageId = uploaded.id;
}
await stateInfoTekno.edit.update();
toast.success("Info teknologi tepat guna berhasil diperbarui!");
router.push("/admin/inovasi/info-teknologi-tepat-guna");
toast.success('Info teknologi tepat guna berhasil diperbarui!');
router.push('/admin/inovasi/info-teknologi-tepat-guna');
} catch (error) {
console.error("Error updating info teknologi tepat guna:", error);
toast.error("Terjadi kesalahan saat memperbarui info teknologi tepat guna");
console.error('Error updating info teknologi tepat guna:', error);
toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna');
}
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Info Teknologi Tepat Guna</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* 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">
Edit Info Teknologi Tepat Guna
</Title>
</Group>
{/* Card form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Judul */}
<TextInput
label="Judul"
placeholder="Masukkan judul info teknologi tepat guna"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
required
/>
{/* Upload gambar */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Deskripsi pakai editor */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -162,7 +195,21 @@ function EditInfoTeknologiTepatGuna() {
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
{/* Tombol submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,8 +1,8 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -32,64 +32,106 @@ function DetailInfoTeknologiTepatGuna() {
if (!stateInfoTekno.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = stateInfoTekno.findUnique.data
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Info Teknologi Tepat Guna</Text>
{stateInfoTekno.findUnique.data ? (
<Paper key={stateInfoTekno.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{stateInfoTekno.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateInfoTekno.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateInfoTekno.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "70%", lg: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Info Teknologi Tepat Guna
</Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Teknologi'}
w={150}
h={150}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Buttons */}
<Group gap="sm" mt={10}>
<Tooltip label="Hapus Info Teknologi" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateInfoTekno.findUnique.data) {
setSelectedId(stateInfoTekno.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateInfoTekno.delete.loading || !stateInfoTekno.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={stateInfoTekno.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Info Teknologi" withArrow position="top">
<Button
onClick={() => {
if (stateInfoTekno.findUnique.data) {
router.push(`/admin/inovasi/info-teknologi-tepat-guna/${stateInfoTekno.findUnique.data.id}/edit`);
}
}}
disabled={!stateInfoTekno.findUnique.data}
color={"green"}
color="green"
onClick={() =>
router.push(`/admin/inovasi/info-teknologi-tepat-guna/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>

View File

@@ -1,7 +1,18 @@
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -11,76 +22,92 @@ import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno';
import { Dropzone } from '@mantine/dropzone';
function CreateInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState)
const stateInfoTekno = useProxy(infoTeknoState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateInfoTekno.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
name: '',
deskripsi: '',
imageId: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
return toast.error('Silahkan pilih file gambar terlebih dahulu');
}
try {
// Upload the image first
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
name: file.name,
});
const uploaded = uploadRes.data?.data
const uploaded = uploadRes.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
return toast.error('Gagal upload gambar');
}
// Set the image ID in the form
stateInfoTekno.create.form.imageId = uploaded.id
stateInfoTekno.create.form.imageId = uploaded.id;
// Submit the form
const success = await stateInfoTekno.create.create()
const success = await stateInfoTekno.create.create();
if (success) {
resetForm()
router.push("/admin/inovasi/info-teknologi-tepat-guna")
resetForm();
router.push('/admin/inovasi/info-teknologi-tepat-guna');
}
} catch (error) {
console.error("Error in handleSubmit:", error)
toast.error("Terjadi kesalahan saat menyimpan data")
console.error('Error in handleSubmit:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
}
};
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Info Teknologi Tepat Guna</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Info Teknologi Tepat Guna
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput
value={stateInfoTekno.create.form.name}
onChange={(val) => {
stateInfoTekno.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Info Teknologi Tepat Guna</Text>}
placeholder="masukkan nama info teknologi tepat guna"
label="Nama Info Teknologi Tepat Guna"
placeholder="Masukkan nama info teknologi tepat guna"
required
/>
{/* Deskripsi */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateInfoTekno.create.form.deskripsi}
onChange={(htmlContent) => {
@@ -88,63 +115,70 @@ function CreateInfoTeknologiTepatGuna() {
}}
/>
</Box>
{/* Upload Gambar */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" c="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Submit Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,13 +1,30 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import infoTeknoState from '../../_state/inovasi/info-tekno';
function InfoTeknologiTepatGuna() {
@@ -15,8 +32,8 @@ function InfoTeknologiTepatGuna() {
return (
<Box>
<HeaderSearch
title='Info Teknologi Tepat Guna'
placeholder='pencarian'
title="Info Teknologi Tepat Guna"
placeholder="Cari info teknologi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,70 +44,125 @@ function InfoTeknologiTepatGuna() {
}
function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState)
const router = useRouter()
const state = useProxy(infoTeknoState);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Info Teknologi Tepat Guna'
href='/admin/inovasi/info-teknologi-tepat-guna/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Info Teknologi Tepat Guna</TableTh>
<TableTh>Deskripsi Singkat Info Teknologi Tepat Guna</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Info Teknologi Tepat Guna</Title>
<Tooltip label="Tambah Info Teknologi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/inovasi/info-teknologi-tepat-guna/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>
Nama Info Teknologi
</TableTh>
<TableTh style={{ width: '50%' }}>
Deskripsi Singkat
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '50%' }}>
<Text
lineClamp={1}
truncate
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`,
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</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>
);
}

View File

@@ -1,62 +1,118 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 React, { useEffect, useState } from 'react';
import { IconListDetails, IconUsers } from '@tabler/icons-react';
function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const router = useRouter();
const pathname = usePathname();
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
const tabs = [
{
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi",
tooltip: "Lihat daftar kolaborasi inovasi",
icon: <IconListDetails size={18} stroke={1.8} />,
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi",
tooltip: "Kelola mitra kolaborasi",
icon: <IconUsers size={18} stroke={1.8} />,
}
];
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
return (
<Stack>
<Title order={3}>Kolaborasi Inovasi</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value);
}
}, [pathname]);
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Kolaborasi Inovasi
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper biar rapi */}
<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>
{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}
</Stack>
);
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsKolaborasi;
export default LayoutTabsKolaborasi;

View File

@@ -7,11 +7,13 @@ import colors from "@/con/colors";
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title
Title,
Tooltip,
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
@@ -25,33 +27,33 @@ function EditKolaborasiInovasi() {
const params = useParams();
const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || '',
deskripsi: kolaborasiState.update.form.deskripsi || '',
tahun: kolaborasiState.update.form.tahun || '',
slug: kolaborasiState.update.form.slug || '',
kolaborator: kolaborasiState.update.form.kolaborator || '',
name: kolaborasiState.update.form.name || "",
deskripsi: kolaborasiState.update.form.deskripsi || "",
tahun: kolaborasiState.update.form.tahun || "",
slug: kolaborasiState.update.form.slug || "",
kolaborator: kolaborasiState.update.form.kolaborator || "",
});
// Load berita by id saat pertama kali
// Load data
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await kolaborasiState.update.load(id); // akses langsung, bukan dari proxy
const data = await kolaborasiState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
tahun: data.tahun || '',
slug: data.slug || '',
kolaborator: data.kolaborator || '',
name: data.name || "",
deskripsi: data.deskripsi || "",
tahun: data.tahun || "",
slug: data.slug || "",
kolaborator: data.kolaborator || "",
});
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
console.error("Error loading kolaborasi:", error);
toast.error("Gagal memuat data kolaborasi inovasi");
}
};
@@ -59,9 +61,7 @@ function EditKolaborasiInovasi() {
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
kolaborasiState.update.form = {
...kolaborasiState.update.form,
name: formData.name,
@@ -71,53 +71,72 @@ function EditKolaborasiInovasi() {
kolaborator: formData.kolaborator,
};
await kolaborasiState.update.submit();
toast.success("Berita berhasil diperbarui!");
toast.success("Kolaborasi inovasi berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi");
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
console.error("Error updating kolaborasi:", error);
toast.error("Terjadi kesalahan saat memperbarui data");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kolaborasi Inovasi</Title>
<Box px={{ base: "sm", md: "lg" }} py="md">
<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 Kolaborasi Inovasi
</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
label="Nama Kolaborasi"
placeholder="Masukkan nama kolaborasi"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi singkat"
required
/>
<TextInput
label="Tahun"
placeholder="Masukkan tahun"
value={formData.tahun}
onChange={(e) => setFormData({ ...formData, tahun: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
required
/>
<TextInput
label="Kolaborator"
placeholder="Masukkan nama kolaborator"
value={formData.kolaborator}
onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>}
placeholder="masukkan kolaborator"
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<Text fw="bold" fz="sm" mb={6}>
Konten
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -126,7 +145,21 @@ function EditKolaborasiInovasi() {
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,118 +1,143 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors';
function DetailKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const kolaborasiState = useProxy(kolaborasiInovasiState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
kolaborasiState.findUnique.load(params?.id as string)
}, [])
kolaborasiState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
kolaborasiState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/kolaborasi-inovasi")
kolaborasiState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
}
}
};
if (!kolaborasiState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = kolaborasiState.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Kolaborasi Inovasi</Text>
{kolaborasiState.findUnique.data ? (
<Paper key={kolaborasiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Kolaborasi Inovasi</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tahun</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.tahun}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Singkat</Text>
<Text fz={"lg"} >{kolaborasiState.findUnique.data?.slug}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kolaborator</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card utama */}
<Paper
withBorder
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Kolaborasi Inovasi
</Text>
{/* Isi detail */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Kolaborasi Inovasi</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tahun</Text>
<Text fz="md" c="dimmed">{data?.tahun || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Kolaborator</Text>
<Text fz="md" c="dimmed">{data?.kolaborator || '-'}</Text>
</Box>
{/* Tombol aksi */}
<Group gap="sm">
<Tooltip label="Hapus Kolaborasi Inovasi" withArrow position="top">
<Button
color="red"
onClick={() => {
if (kolaborasiState.findUnique.data) {
setSelectedId(kolaborasiState.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={kolaborasiState.delete.loading || !kolaborasiState.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={kolaborasiState.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Kolaborasi Inovasi" withArrow position="top">
<Button
onClick={() => {
if (kolaborasiState.findUnique.data) {
router.push(`/admin/inovasi/kolaborasi-inovasi/${kolaborasiState.findUnique.data.id}/edit`);
}
}}
disabled={!kolaborasiState.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
{/* Modal konfirmasi hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kolaborasi inovasi ini?'
text="Apakah anda yakin ingin menghapus kolaborasi inovasi ini?"
/>
</Box>
);
}
export default DetailKolaborasiInovasi;
export default DetailKolaborasiInovasi;

View File

@@ -3,7 +3,7 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
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 { YearPickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -11,10 +11,8 @@ import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState)
const stateCreate = useProxy(kolaborasiInovasiState);
const router = useRouter();
const resetForm = () => {
@@ -24,10 +22,10 @@ function CreateProgramKreatifDesa() {
slug: "",
deskripsi: "",
kolaborator: "",
}
}
};
};
// Generate slug from name
// Generate slug dari name
useEffect(() => {
const { name } = stateCreate.create.form;
if (name) {
@@ -42,67 +40,89 @@ function CreateProgramKreatifDesa() {
const handleSubmit = async () => {
try {
// Submit data kolaborasi inovasi
await stateCreate.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi");
router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
toast.success("Berhasil menambahkan kolaborasi inovasi");
} catch (error) {
console.error("Error creating kolaborasi inovasi:", error);
toast.error("Terjadi kesalahan saat menyimpan data");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Kolaborasi Inovasi</Title>
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">
Tambah Kolaborasi Inovasi
</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
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
placeholder="masukkan nama kolaborasi inovasi"
label={<Text fz="sm" fw="bold">Nama Kolaborasi Inovasi</Text>}
placeholder="Masukkan nama kolaborasi inovasi"
value={stateCreate.create.form.name || ''}
onChange={(val) => stateCreate.create.form.name = val.target.value}
required
/>
<YearPickerInput
clearable
value={stateCreate.create.form.tahun ? new Date(stateCreate.create.form.tahun, 0, 1) : null}
label="Tahun"
label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Pilih tahun"
onChange={(dateString: string) => {
const year = dateString ? new Date(dateString).getFullYear() : 0;
onChange={(date) => {
const year = date ? new Date(date).getFullYear() : 0;
stateCreate.create.form.tahun = year;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(val) => {
stateCreate.create.form.deskripsi = val;
}}
onChange={(val) => stateCreate.create.form.deskripsi = val}
/>
</Box>
<TextInput
label={<Text fz="sm" fw="bold">Kolaborator</Text>}
placeholder="Masukkan kolaborator"
value={stateCreate.create.form.kolaborator || ''}
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
placeholder='Masukkan kolaborator'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,10 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
@@ -15,8 +32,8 @@ function KolaborasiInovasi() {
return (
<Box>
<HeaderSearch
title='Kolaborasi Inovasi'
placeholder='pencarian'
title="Kolaborasi Inovasi"
placeholder="Cari kolaborasi inovasi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,101 +44,116 @@ function KolaborasiInovasi() {
}
function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState)
const listState = useProxy(kolaborasiInovasiState);
const router = useRouter();
const {
data,
loading,
page,
totalPages,
load,
} = listState.findMany
const { data, loading, page, totalPages, load } = listState.findMany;
useEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>{item.tahun}</TableTd>
<TableTd>{item.slug}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kolaborasi Inovasi</Title>
<Tooltip label="Tambah Kolaborasi Inovasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '15%' }}>Tahun</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
{item.tahun}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate="end" lineClamp={1} c="dimmed">
{item.slug}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box >
</Box>
);
}

View File

@@ -3,24 +3,41 @@
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import {
IconArrowBack,
IconImageInPicture,
IconPhoto,
IconUpload,
IconX,
} from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditFoto() {
const state = useProxy(mitraKolaborasi)
function EditMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: state.update.form.name || '',
imageId: state.update.form.imageId || ''
imageId: state.update.form.imageId || '',
});
useEffect(() => {
@@ -32,7 +49,7 @@ function EditFoto() {
if (data) {
setFormData({
name: data.name || '',
imageId: data.imageId || ''
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
@@ -51,7 +68,7 @@ function EditFoto() {
state.update.form = {
...state.update.form,
name: formData.name,
imageId: formData.imageId
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
@@ -60,7 +77,7 @@ function EditFoto() {
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal upload gambar');
}
state.update.form.imageId = uploaded.id;
}
@@ -74,70 +91,114 @@ function EditFoto() {
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Mitra
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Mitra</Title>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Nama */}
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
placeholder='Masukkan nama mitra'
label="Nama Mitra"
placeholder="Masukkan nama mitra"
value={formData.name}
onChange={(e) =>
(formData.name = e.target.value)
}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
{/* Upload Foto */}
<Box>
<Text>Upload Foto</Text>
<Text fw="bold" fz="sm" mb={6}>
Upload Foto
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
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')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</div>
</Stack>
</Group>
</Dropzone>
{/* Preview Foto */}
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} loading="lazy"/>
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
<Center w={200} h={200} bg="gray.1" mt="sm" style={{ borderRadius: 8 }}>
<IconImageInPicture size={48} color="#868e96" />
</Center>
)}
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
{/* Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
@@ -145,4 +206,4 @@ function EditFoto() {
);
}
export default EditFoto;
export default EditMitraKolaborasi;

View File

@@ -2,7 +2,18 @@
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -10,27 +21,24 @@ import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateFoto() {
const state = useProxy(mitraKolaborasi)
function CreateMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
state.create.form = {
name: "",
imageId: "",
name: '',
imageId: '',
};
setPreviewImage(null)
setFile(null)
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
@@ -40,93 +48,110 @@ function CreateFoto() {
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi")
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + 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 Mitra Kolaborasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Mitra</Title>
{/* Card Wrapper */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Nama Mitra */}
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
placeholder='Masukkan nama mitra'
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.target.value;
}}
label="Nama Mitra"
placeholder="Masukkan nama mitra"
value={state.create.form.name || ''}
onChange={(e) => (state.create.form.name = e.target.value)}
required
/>
{/* Upload Image */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>
Gambar Mitra
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
{/* Submit Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
@@ -134,4 +159,4 @@ function CreateFoto() {
);
}
export default CreateFoto;
export default CreateMitraKolaborasi;

View File

@@ -1,13 +1,31 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconEdit, IconSearch, IconX, IconPlus } 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 JudulList from '../../../_com/judulList';
import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
@@ -16,8 +34,8 @@ function MitraKolaborasi() {
return (
<Box>
<HeaderSearch
title='Mitra Kolaborasi'
placeholder='pencarian'
title="Mitra Kolaborasi"
placeholder="Cari nama mitra..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,160 +46,169 @@ function MitraKolaborasi() {
}
function ListMitraKolaborasi({ search }: { search: string }) {
const listState = useProxy(mitraKolaborasi)
const listState = useProxy(mitraKolaborasi);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const handleHapus = () => {
if (selectedId) {
mitraKolaborasi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/kolaborasi-inovasi")
mitraKolaborasi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/inovasi/kolaborasi-inovasi');
}
}
};
const {
data,
loading,
page,
totalPages,
load,
} = listState.findMany
const { data, loading, page, totalPages, load } = listState.findMany;
useEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data mitra kolaborasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box style={{
width: 100,
height: 100,
position: 'relative',
overflow: 'hidden',
borderRadius: 4
}}>
<Image
src={item.image?.link || ''}
alt={item.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center'
}}
loading="lazy"
/>
</Box>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={mitraKolaborasi.delete.loading || !item}
color={"red"}
>
<IconX size={20} />
</Button>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
router.push(`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`);
}
}}
disabled={!item}
color={"green"}
>
<IconEdit size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Mitra Kolaborasi</Title>
<Tooltip label="Tambah Mitra Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh style={{ width: '30%' }}>Nama Mitra</TableTh>
<TableTh style={{ width: '25%' }}>Image</TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh>
<TableTh style={{ width: '15%' }}>Edit</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box
w={70}
h={70}
>
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
/>
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd>
<Tooltip label="Hapus Mitra" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="red"
disabled={mitraKolaborasi.delete.loading || !item}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={16} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Edit Mitra" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="green"
disabled={!item}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
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 */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus mitra kolaborasi ini?'
text="Apakah anda yakin ingin menghapus mitra kolaborasi ini?"
/>
</Box >
</Box>
);
}

View File

@@ -4,7 +4,6 @@ import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
@@ -16,11 +15,10 @@ import {
TableThead,
TableTr,
Text,
Title,
Tooltip
Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -66,27 +64,13 @@ function ListAdministrasiOnline({ search }: { search: string }) {
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Administrasi Online</Title>
<Tooltip label="Tambah Layanan Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/inovasi/layanan-online-desa/administrasi-online/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Layanan</TableTh>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Alamat</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -32,60 +32,88 @@ function DetailJenisLayanan() {
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = state.findUnique.data
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Jenis Layanan</Text>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }}></Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card utama */}
<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 Jenis Layanan
</Text>
{/* Detail isi */}
<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">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
{/* Tombol aksi */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Jenis Layanan" withArrow position="top">
<Button
color="red"
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id)
setModalHapus(true)
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={state.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Jenis Layanan" withArrow position="top">
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -94,7 +122,7 @@ function DetailJenisLayanan() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus jenis layanan ini?'
text='Apakah Anda yakin ingin menghapus jenis layanan ini?'
/>
</Box>
);

View File

@@ -2,7 +2,17 @@
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
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 { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -10,7 +20,7 @@ import { useProxy } from 'valtio/utils';
function CreateJenisLayanan() {
const router = useRouter();
const statePasar = useProxy(layananonlineDesa.jenisLayanan)
const statePasar = useProxy(layananonlineDesa.jenisLayanan);
useEffect(() => {
statePasar.findMany.load();
@@ -18,51 +28,81 @@ function CreateJenisLayanan() {
const resetForm = () => {
statePasar.create.form = {
nama: "",
deskripsi: "",
nama: '',
deskripsi: '',
};
}
};
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
}
router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
};
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<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>
</Box>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Layanan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Layanan</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
{/* 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
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw="bold" fz="sm">Nama Jenis Layanan</Text>}
placeholder="Masukkan nama jenis layanan"
required
/>
<TextInput
value={statePasar.create.form.deskripsi}
onChange={(val) => {
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw="bold" fz="sm">Deskripsi</Text>}
placeholder="Masukkan deskripsi"
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 Jenis Layanan</Text>}
placeholder='Masukkan nama jenis layanan'
/>
<TextInput
value={statePasar.create.form.deskripsi}
onChange={(val) => {
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -2,7 +2,16 @@
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -70,24 +79,55 @@ function EditJenisPengaduan() {
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header + tombol back */}
<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 Jenis Pengaduan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pengaduan</Title>
{/* 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
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
required
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -2,7 +2,16 @@
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -10,7 +19,7 @@ import { useProxy } from 'valtio/utils';
function CreateJenisPengaduan() {
const router = useRouter();
const state = useProxy(layananonlineDesa.jenisPengaduan)
const state = useProxy(layananonlineDesa.jenisPengaduan);
useEffect(() => {
state.findMany.load();
@@ -18,42 +27,64 @@ function CreateJenisPengaduan() {
const resetForm = () => {
state.create.form = {
nama: "",
nama: '',
};
}
};
const handleSubmit = async () => {
await state.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan")
}
router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
};
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Pengaduan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Pengaduan</Title>
<TextInput
value={state.create.form.nama}
onChange={(val) => {
state.create.form.nama = val.target.value;
{/* 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
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
value={state.create.form.nama || ''}
onChange={(e) => (state.create.form.nama = e.target.value)}
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 Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,25 +1,40 @@
'use client'
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 { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisPengaduan() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Jenis Pengaduan'
placeholder='pencarian'
title="Jenis Pengaduan"
placeholder="Cari nama jenis pengaduan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,87 +45,138 @@ function JenisPengaduan() {
}
function ListJenisPengaduan({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.jenisPengaduan)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const state = useProxy(layananonlineDesa.jenisPengaduan);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const { data, page, totalPages, loading, load, } = state.findMany;
useShallowEffect(() => {
state.findMany.load()
}, [])
load(page, 10, search);
}, [page, search]);
const handleHapus = async () => {
if (selectedId) {
await state.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
setModalHapus(false);
setSelectedId(null);
}
}
};
const filteredData = data || []
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenis Pengaduan'
href='/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Pengaduan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jenis Pengaduan</Title>
<Tooltip label="Tambah Jenis Pengaduan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '60%' }}>Nama Jenis Pengaduan</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{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/inovasi/layanan-online-desa/jenis-pengaduan/${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 color="dimmed">
Tidak ada jenis pengaduan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</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 Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -1,7 +1,6 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -11,102 +10,143 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailPengaduanMasyarakat() {
const pengaduanState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const pengaduanState = useProxy(layananonlineDesa);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string)
}, [])
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat")
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat");
}
}
};
if (!pengaduanState.pengaduanMasyarakat.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = pengaduanState.pengaduanMasyarakat.findUnique.data;
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Pengaduan Masyarakat</Text>
<Button
onClick={() => {
if (pengaduanState.pengaduanMasyarakat.findUnique.data) {
setSelectedId(pengaduanState.pengaduanMasyarakat.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={pengaduanState.pengaduanMasyarakat.delete.loading || !pengaduanState.pengaduanMasyarakat.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{pengaduanState.pengaduanMasyarakat.findUnique.data ? (
<Paper key={pengaduanState.pengaduanMasyarakat.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Email</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nik}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.judulPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi Kejadian</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.lokasiKejadian}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.deskripsiPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.jenisPengaduan?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={pengaduanState.pengaduanMasyarakat.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
</Box>
</Stack>
</Paper>
) : null}
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card Detail */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Judul Halaman */}
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pengaduan Masyarakat
</Text>
{/* Isi Data */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Email</Text>
<Text fz="md" c="dimmed">{data?.email || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nomor Telepon</Text>
<Text fz="md" c="dimmed">{data?.nomorTelepon || '-'}</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">Judul Pengaduan</Text>
<Text fz="md" c="dimmed">{data?.judulPengaduan || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Lokasi Kejadian</Text>
<Text fz="md" c="dimmed">{data?.lokasiKejadian || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi Pengaduan</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsiPengaduan || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Pengaduan</Text>
<Text fz="md" c="dimmed">{data?.jenisPengaduan?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Pengaduan"
w={120}
h={120}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Button */}
<Group gap="sm">
<Tooltip label="Hapus Pengaduan" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={pengaduanState.pengaduanMasyarakat.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -115,10 +155,10 @@ function DetailPengaduanMasyarakat() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
text="Apakah anda yakin ingin menghapus pengaduan masyarakat ini?"
/>
</Box>
);
}
export default DetailPengaduanMasyarakat;
export default DetailPengaduanMasyarakat;

View File

@@ -1,6 +1,24 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
@@ -16,7 +34,7 @@ function PengaduanMasyarakat() {
<Box>
<HeaderSearch
title='Pengaduan Masyarakat'
placeholder='pencarian'
placeholder='Cari nama, email, atau nomor telepon...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,7 +45,7 @@ function PengaduanMasyarakat() {
}
function ListPengaduanMasyarakat({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat)
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat);
const router = useRouter();
const {
data,
@@ -38,58 +56,89 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
} = listState.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
load(page, 10, search);
}, [page, search]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.email.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (loading || !data) {
return <Skeleton h={500} />;
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Pengaduan Masyarakat</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Email</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody style={{ overflowX: "auto" }}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pengaduan Masyarakat</Title>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Email</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.email}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.nomorTelepon}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Tooltip label="Lihat detail pengaduan" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data pengaduan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
))}
)}
</TableTbody>
</Table>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -3,7 +3,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -19,17 +29,17 @@ interface FormProgramKreatif {
icon: string;
}
function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState)
const params = useParams()
const stateProgramKreatif = useProxy(programKreatifState);
const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
})
});
useEffect(() => {
const loadProgramKreatif = async () => {
@@ -39,16 +49,13 @@ function EditProgramKreatifDesa() {
try {
const data = await stateProgramKreatif.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = {
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
slug: data.slug,
@@ -57,16 +64,14 @@ function EditProgramKreatifDesa() {
});
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
console.error('Error loading program kreatif:', error);
toast.error('Gagal memuat data program kreatif');
}
}
};
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramKreatif.update.form = {
@@ -75,49 +80,57 @@ function EditProgramKreatifDesa() {
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
icon: formData.icon.trim(),
}
};
await stateProgramKreatif.update.submit();
router.push("/admin/inovasi/program-kreatif-desa");
router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) {
console.error("Error updating program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
console.error('Error updating program kreatif:', error);
toast.error('Gagal memuat data program kreatif');
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Edit Program Kreatif Desa</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<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 Kreatif Desa
</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 Program Kreatif Desa"
placeholder="Masukkan nama program kreatif desa"
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Deskripsi Singkat Program Kreatif Desa"
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={formData.slug}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="masukkan deskripsi singkat program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
slug: val.target.value
})
}}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -126,8 +139,11 @@ function EditProgramKreatifDesa() {
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
@@ -135,10 +151,22 @@ function EditProgramKreatifDesa() {
stateProgramKreatif.update.form.icon = value;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,8 +1,8 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,8 +10,6 @@ import { IconKey, IconMapper } from '../../../_com/iconMap';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramKreatif = useProxy(programKreatifState)
@@ -34,74 +32,104 @@ function DetailProgramKreatifDesa() {
if (!stateProgramKreatif.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kreatif Desa</Text>
const data = stateProgramKreatif.findUnique.data
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
return (
<Box py={10}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Kreatif Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.name}</Text>
<Text fz="lg" fw="bold">Nama Program Kreatif Desa</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{stateProgramKreatif.findUnique.data?.icon && (
<Text fz="lg" fw="bold">Ikon Program Kreatif Desa</Text>
{data?.icon ? (
<IconMapper
name={stateProgramKreatif.findUnique.data?.icon as IconKey}
name={data.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.slug}</Text>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramKreatif.findUnique.data?.deskripsi }}></Text>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Flex gap="sm" mt={10}>
<Tooltip label="Hapus Program Kreatif Desa" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
setSelectedId(stateProgramKreatif.findUnique.data.id);
if (data) {
setSelectedId(data.id);
setModalHapus(true);
}
}}
disabled={stateProgramKreatif.delete.loading || !stateProgramKreatif.findUnique.data}
color={"red"}
disabled={stateProgramKreatif.delete.loading || !data}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Program Kreatif Desa" withArrow position="top">
<Button
color="green"
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
router.push(`/admin/inovasi/program-kreatif-desa/${stateProgramKreatif.findUnique.data.id}/edit`);
if (data) {
router.push(`/admin/inovasi/program-kreatif-desa/${data.id}/edit`);
}
}}
disabled={!stateProgramKreatif.findUnique.data}
color={"green"}
disabled={!data}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Flex>
</Stack>
</Paper>
</Stack>
@@ -112,7 +140,7 @@ function DetailProgramKreatifDesa() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kreatif desa ini?"
text="Apakah Anda yakin ingin menghapus program kreatif desa ini?"
/>
</Box>
);

View File

@@ -1,6 +1,16 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -8,9 +18,8 @@ import CreateEditor from '../../../_com/createEditor';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../../../_com/selectIcon';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState)
const stateCreate = useProxy(programKreatifState);
const router = useRouter();
const resetForm = () => {
@@ -19,48 +28,90 @@ function CreateProgramKreatifDesa() {
slug: "",
deskripsi: "",
icon: "",
}
}
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
router.push("/admin/inovasi/program-kreatif-desa");
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Program Kreatif Desa</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol kembali */}
<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 Kreatif 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
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => stateCreate.create.form.name = val.target.value}
label={<Text fw="bold" fz="sm">Nama Program Kreatif Desa</Text>}
placeholder="Masukkan nama program kreatif desa"
value={stateCreate.create.form.name || ""}
onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<TextInput
label={<Text fw="bold" fz="sm">Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={stateCreate.create.form.slug || ""}
onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Program Kreatif Desa
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
{/* Tombol Submit */}
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,31 +1,64 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconCash, IconChristmasTreeFilled, IconClipboard, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconHospital, IconScale, IconSchool, IconSearch, IconShieldFilled, IconShoppingCart, IconTrash, IconTree, IconTrendingUp, IconTruck } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import programKreatifState from '../../_state/inovasi/program-kreatif';
import { useProxy } from 'valtio/utils';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboard,
IconDeviceImac,
IconDroplet,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconPlus,
IconRecycle,
IconScale,
IconSchool,
IconSearch,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrash,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruck,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKreatifState from '../../_state/inovasi/program-kreatif';
function ProgramKreatifDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Kreatif Desa'
placeholder='pencarian'
title="Program Kreatif Desa"
placeholder="Cari program kreatif..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -36,23 +69,15 @@ function ProgramKreatifDesa() {
}
function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState)
const { data, loading, page, totalPages, load } = listState.findMany
const listState = useProxy(programKreatifState);
const { data, loading, page, totalPages, load } = listState.findMany;
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
load(page, 10, search);
}, [page, search]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -74,26 +99,40 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital
layananKesehatan: IconHospital,
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={650} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Paper p="md" radius="md" shadow="sm" withBorder>
<Stack>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Tooltip label="Tambah Program Kreatif Desa" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Table highlightOnHover striped>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
@@ -104,27 +143,52 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program kreatif desa yang tersedia</Text>
<Text ta="center" c="dimmed" py="lg">
Tidak ada data program kreatif desa yang tersedia
</Text>
</Stack>
</Paper>
</Box >
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Paper
withBorder
bg={colors['white-1']}
p="lg"
shadow="md"
radius="md"
h={{ base: 'auto', md: 650 }}
>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Tooltip label="Tambah Program Kreatif Desa" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<Table highlightOnHover striped >
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh>
<TableTh style={{ width: '20%' }}>
<Text lineClamp={1} fw={"bold"} fz="sm">Nama Program Kreatif Desa</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '10%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
@@ -132,9 +196,19 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.slug }}></TableTd>
<TableTd style={{ width: '10%' }}>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
<Box w={150}>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.slug}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '10%', textAlign: 'center' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
@@ -142,8 +216,17 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)}>
<IconDeviceImac size={25} />
<Button
size="xs"
radius="md"
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)
}
>
<IconDeviceImac size={18} />
<Text ml={6}>Detail</Text>
</Button>
</TableTd>
</TableTr>
@@ -157,14 +240,17 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default ProgramKreatifDesa;

View File

@@ -26,6 +26,7 @@ import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditKontakDaruratKeamanan() {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const params = useParams();
@@ -37,29 +38,31 @@ function EditKontakDaruratKeamanan() {
// Load data
useEffect(() => {
kontakDarurat.kontakDaruratItem.findMany.load();
const loadKontakDarurat = async () => {
const id = params?.id as string;
if (!id) return;
const loadData = async () => {
try {
const data = await kontakState.update.load(id);
if (data) {
setFormData({
name: data.nama || "",
icon: data.icon || "",
kategoriId: data.kategoriId || [],
});
setIsLoading(true);
await kontakDarurat.kontakDaruratItem.findMany.load();
const id = params?.id as string;
if (id) {
const data = await kontakState.update.load(id);
if (data) {
setFormData({
name: data.nama || "",
icon: data.icon || "",
kategoriId: data.kategoriId || [],
});
}
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat");
console.error("Error loading data:", error);
toast.error("Gagal memuat data");
} finally {
setIsLoading(false);
}
};
loadKontakDarurat();
loadData();
}, [params?.id]);
// Handle submit
const handleSubmit = async () => {
try {
@@ -120,17 +123,20 @@ function EditKontakDaruratKeamanan() {
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
label={<Text fw={"bold"} fz={"sm"}>Kontak Item</Text>}
placeholder='Pilih kontak item'
placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"}
data={
kontakDarurat.kontakDaruratItem.findMany.data?.map((v) => ({
value: v.id, // Make sure this is using the ID
label: v.nama
})) || []
Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data)
? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({
value: v.id,
label: v.nama
}))
: []
}
clearable
searchable
required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
disabled={isLoading}
/>
<Box>

View File

@@ -148,12 +148,16 @@ function EditLaporanPublik() {
required
/>
<TextInput
value={formData.kronologi}
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })}
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>}
placeholder="Masukkan kronologi laporan publik"
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>Kronologi Laporan Publik</Text>
<EditEditor
value={formData.kronologi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, kronologi: htmlContent }));
stateLaporan.edit.form.kronologi = htmlContent;
}}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text>

View File

@@ -104,7 +104,9 @@ function ListProgramKesehatan({ search }: { search: string }) {
</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
<Box w={200}>
<Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>

View File

@@ -1,19 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function ajukanIdeInovatifFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
];
}
export default async function ajukanIdeInovatifFindMany() {
try {
const data = await prisma.ajukanIdeInovatif.findMany({});
const [data, total] = await Promise.all([
prisma.ajukanIdeInovatif.findMany({
where: where, // Use the where clause that includes search conditions
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.ajukanIdeInovatif.count({
where: where // Also update the count query to use the same where clause
})
]);
return {
success: true,
message: "Success fetch ajukan ide inovatif",
message: "Success fetch ajukan ide inovatif with pagination",
data,
page,
limit,
totalPages: Math.ceil(total / limit),
total,
};
} catch (error) {
console.error("Find many error:", error);
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch ajukan ide inovatif",
message: "Failed fetch ajukan ide inovatif with pagination",
};
}
}
export default ajukanIdeInovatifFindMany;

View File

@@ -23,16 +23,16 @@ async function administrasiOnlineFindMany(context: Context) {
try {
const [data, total] = await Promise.all([
prisma.administrasiOnline.findMany({
where: { isActive: true },
where: where, // Use the where clause that includes search conditions
include: {
jenisLayanan: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
orderBy: { createdAt: 'desc' },
}),
prisma.administrasiOnline.count({
where: { isActive: true }
where: where // Also update the count query to use the same where clause
})
]);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
@@ -5,12 +6,24 @@ import { Context } from "elysia";
async function pengaduanMasyarakatFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ judulPengaduan: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.pengaduanMasyarakat.findMany({
where: { isActive: true },
where: where,
include: {
jenisPengaduan: true,
image: true,
@@ -20,7 +33,7 @@ async function pengaduanMasyarakatFindMany(context: Context) {
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.pengaduanMasyarakat.count({
where: { isActive: true }
where: where
})
]);
@@ -29,6 +42,7 @@ async function pengaduanMasyarakatFindMany(context: Context) {
message: "Success fetch pengaduan masyarakat with pagination",
data,
page,
limit,
totalPages: Math.ceil(total / limit),
total,
};

View File

@@ -1,15 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function jenisPengaduanFindMany() {
const data = await prisma.jenisPengaduan.findMany();
return {
export default async function jenisPengaduanFindMany(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' } }
];
}
try {
const [data, total] = await Promise.all([
prisma.jenisPengaduan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.jenisLayanan.count({ where }),
]);
return {
success: true,
data: data.map((item: any) => {
return {
id: item.id,
nama: item.nama,
nama: item.nama
}
}),
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data jenis pengaduan",
};
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
@@ -5,29 +6,39 @@ import { Context } from "elysia";
export default async function programKreatifFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.programKreatif.findMany({
where: { isActive: true },
where: where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.programKreatif.count({
where: { isActive: true }
where: where
})
]);
const totalPages = Math.ceil(total / limit);
return {
success: true,
message: "Success fetch program kreatif with pagination",
data,
page,
totalPages,
limit,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
@@ -35,10 +46,6 @@ export default async function programKreatifFindMany(context: Context) {
return {
success: false,
message: "Failed fetch program kreatif with pagination",
data: [],
page: 1,
totalPages: 1,
total: 0,
};
}
}

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
};
async function createPendaftaran(context: Context) {
const body = context.body as FormCreate;
await prisma.pendaftaranJadwalKegiatan.create({
data: {
name: body.name,
tanggal: body.tanggal,
namaOrangtua: body.namaOrangtua,
nomor: body.nomor,
alamat: body.alamat,
catatan: body.catatan,
},
});
return {
success: true,
message: "Success create pendaftaran jadwal kegiatan",
data: {
...body,
},
};
}
export default createPendaftaran;

View File

@@ -0,0 +1,23 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pendaftaranJadwalKegiatanDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
await prisma.pendaftaranJadwalKegiatan.delete({
where: { id },
});
return {
success: true,
message: "Pendaftaran jadwal kegiatan berhasil dihapus",
status: 200,
};
}

View File

@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pendaftaranJadwalKegiatanFindMany(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 = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.pendaftaranJadwalKegiatan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.pendaftaranJadwalKegiatan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil pendaftaran jadwal kegiatan 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 pendaftaran jadwal kegiatan",
};
}
}

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pendaftaranJadwalKegiatanFindUnique(context: Context) {
const id = context.params?.id as string;
if (!id) {
return Response.json({
success: false,
message: "ID tidak ditemukan",
}, {status: 400});
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, {status: 400});
}
const data = await prisma.pendaftaranJadwalKegiatan.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Pendaftaran jadwal kegiatan tidak ditemukan",
}, {status: 404});
}
return Response.json({
success: true,
message: "Success fetch pendaftaran jadwal kegiatan by ID",
data,
}, {status: 200});
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil pendaftaran jadwal kegiatan: " + (error instanceof Error ? error.message : 'Unknown error'),
}, {status: 500});
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import pendaftaranJadwalKegiatanCreate from "./create";
import pendaftaranJadwalKegiatanDelete from "./del";
import pendaftaranJadwalKegiatanUpdate from "./updt";
import pendaftaranJadwalKegiatanFindUnique from "./findUnique";
import pendaftaranJadwalKegiatanFindMany from "./findMany";
const PendaftaranJadwalKegiatan = new Elysia({
prefix: "/pendaftaran-jadwal-kegiatan",
tags: ["Kesehatan/Pendaftaran Jadwal Kegiatan"],
})
.post("/create", pendaftaranJadwalKegiatanCreate, {
body: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
})
.get("/find-many", pendaftaranJadwalKegiatanFindMany)
.get("/:id", pendaftaranJadwalKegiatanFindUnique)
.delete("/del/:id", pendaftaranJadwalKegiatanDelete)
.put(
"/:id",
pendaftaranJadwalKegiatanUpdate,
{
body: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
}
);
export default PendaftaranJadwalKegiatan;

View File

@@ -0,0 +1,87 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
id: string;
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
}
export default async function mitraKolaborasiUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
tanggal,
namaOrangtua,
nomor,
alamat,
catatan
} = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak ditemukan",
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
})
}
const existing = await prisma.pendaftaranJadwalKegiatan.findUnique({
where: {id},
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Pendaftaran jadwal kegiatan tidak ditemukan",
}), {
status: 404,
headers: {
'Content-Type': 'application/json'
}
})
}
const updated = await prisma.pendaftaranJadwalKegiatan.update({
where: { id },
data: {
tanggal,
namaOrangtua,
nomor,
alamat,
catatan,
}
})
return new Response(JSON.stringify({
success: true,
message: "Pendaftaran jadwal kegiatan berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error("Error updating pendaftaran jadwal kegiatan:", error);
return new Response(JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate pendaftaran jadwal kegiatan",
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
})
}
}

View File

@@ -19,6 +19,7 @@ import ArtikelKesehatan from "./data_kesehatan_warga/artikel_kesehatan";
import Kelahiran from "./data_kesehatan_warga/persentase_kelahiran_kematian/kelahiran";
import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kematian";
import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis";
import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
const Kesehatan = new Elysia({
@@ -45,4 +46,5 @@ const Kesehatan = new Elysia({
.use(Kelahiran)
.use(Kematian)
.use(DokterTenagaMedis)
.use(PendaftaranJadwalKegiatan)
export default Kesehatan;

View File

@@ -1,58 +1,108 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Image, ListItem, List } from '@mantine/core';
import React from 'react';
import { Box, Center, Image, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
<Box px={{ base: "md", md: 100 }}><BackButton /></Box>
<Container w={{ base: "100%", md: "50%" }} >
<Box pb={20}>
<Text ta={"center"} fz={"3.4rem"} c={colors["blue-button"]} fw={"bold"}>
Taman Beji Cengana
</Text>
<Text
ta={"center"}
fw={"bold"}
fz={"1.5rem"}
>
Informasi dan Pelayanan Administrasi Digital
</Text>
</Box>
<Image src="/api/img/taman-beji.jpg" alt='' w={"100%"} h={400} />
</Container>
<Box px={{ base: "md", md: 100 }}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"}>
Taman Beji Cengana, terletak di Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali, adalah situs suci yang memiliki nilai spiritual dan sejarah yang tinggi. Tempat ini dikenal sebagai lokasi untuk ritual pembersihan diri (melukat) dan peribadatan oleh umat Hindu Bali. Keberadaan mata air suci (Tirta Klebutan) di Taman Beji Cengana dipercaya memberikan berkah dan penyucian bagi mereka yang datang untuk berdoa dan melakukan ritual.
</Text>
<Text fz={{ base: "sm", md: "lg" }} ta={"justify"}>
Potensi Desa melalui Taman Beji Cengana:
</Text>
<List py={20} type='ordered'>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pengembangan Pariwisata Spiritual:</Text> Taman Beji Cengana memiliki potensi besar sebagai destinasi wisata spiritual. Wisatawan yang mencari pengalaman spiritual dan ketenangan batin dapat tertarik untuk mengunjungi tempat ini, mengikuti ritual melukat, dan merasakan suasana sakral yang ditawarkan.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pelestarian Budaya dan Tradisi:</Text>Dengan mempromosikan Taman Beji Cengana sebagai pusat kegiatan budaya dan ritual tradisional, desa dapat memastikan bahwa warisan budaya dan tradisi lokal tetap lestari.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pendidikan dan Penelitian:</Text>Taman Beji Cengana dapat dijadikan sebagai pusat pendidikan dan penelitian bagi akademisi, peneliti, dan pelajar yang tertarik mempelajari budaya, agama, dan sejarah Bali.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pengembangan Ekonomi Kreatif:</Text>Dengan meningkatnya jumlah pengunjung ke Taman Beji Cengana, peluang bagi pengembangan ekonomi kreatif juga terbuka lebar. Masyarakat lokal dapat mengembangkan produk kerajinan tangan, kuliner khas, dan suvenir yang mencerminkan budaya dan tradisi desa.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Konservasi Lingkungan:</Text>Sebagai situs suci dengan mata air alami, Taman Beji Cengana memiliki peran penting dalam konservasi lingkungan. Upaya menjaga kebersihan dan kelestarian mata air serta lingkungan sekitarnya dapat menjadi contoh praktik konservasi yang baik.
</ListItem>
</List>
<Text fz={{ base: "sm", md: "lg" }} ta={"justify"}>
Dengan memanfaatkan potensi yang dimiliki Taman Beji Cengana, Desa Darmasaba dapat mengembangkan sektor pariwisata, budaya, pendidikan, ekonomi, dan lingkungan secara berkelanjutan, yang pada gilirannya akan meningkatkan kesejahteraan masyarakat dan pelestarian warisan budaya.
</Text>
</Box>
</Stack>
);
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() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Keamanan Lingkungan (Pecalang / Patwal)
</Text>
<Text px={{ base: 20, md: 150 }} ta={"center"} fz={{ base: "h4", md: "h3" }} >
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>
</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>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -120,7 +120,7 @@ function Page() {
<Box>
<Text fw={600} fz="lg" >Kronologi</Text>
<Text fz="sm" c="dimmed">{data.kronologi || '-'}</Text>
<Text fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: data.kronologi || '-' }} />
</Box>
<Divider />

View File

@@ -55,27 +55,28 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center">
<BackButton />
<Flex gap={"xs"} align={"center"}>
<TextInput
placeholder="Cari laporan"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<Button
onClick={open}
bg={colors['blue-button']}
size="md"
radius="md"
>
<IconPlus size={20} />
</Button>
</Flex>
<TextInput
placeholder="Cari laporan"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Flex>
</Box>
<Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Laporan Keamanan Lingkungan
</Text>
<Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between">
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Laporan Keamanan Lingkungan
</Text>
<Button
onClick={open}
bg={colors['blue-button']}
size="md"
radius="md"
rightSection={<IconPlus size={20} />}
>
Tambah Laporan
</Button>
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>

View File

@@ -1,11 +1,129 @@
import React from 'react';
'use client'
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { Box, Button, Card, Center, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowLeft } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function DetailPencegahanKriminalitas() {
const router = useRouter();
const params = useParams();
const kriminalitasState = useProxy(pencegahanKriminalitasState);
useShallowEffect(() => {
kriminalitasState.findUnique.load(params?.id as string);
}, []);
if (kriminalitasState.findUnique.loading) {
return (
<Center py="xl">
<Loader size="lg" color="blue" />
</Center>
);
}
if (!kriminalitasState.findUnique.data) {
return (
<Center h={400}>
<Stack align="center" gap="sm">
<Text fz="lg" fw="bold" c="dimmed">Data tidak ditemukan</Text>
<Button
variant="light"
color="blue"
leftSection={<IconArrowLeft size={18} />}
onClick={() => router.push("/admin/keamanan/pencegahan-kriminalitas")}
>
Kembali ke daftar
</Button>
</Stack>
</Center>
);
}
const data = kriminalitasState.findUnique.data;
function Page() {
return (
<div>
Page
</div>
<Box py="md" px="md">
<Group mb="md">
<Button
variant="light"
color="blue"
onClick={() => router.back()}
leftSection={<IconArrowLeft size={20} />}
>
Kembali
</Button>
</Group>
<Card
withBorder
radius="xl"
shadow="md"
p="xl"
bg="white"
>
<Stack gap="lg">
<Title order={2} c="blue">Detail Pencegahan Kriminalitas</Title>
<Paper radius="lg" p="lg" withBorder>
<Stack gap="lg">
<Stack gap={4}>
<Text fz="sm" c="dimmed">Judul</Text>
<Text fz="lg" fw={600}>{data?.judul || '-'}</Text>
</Stack>
<Stack gap={4}>
<Text fz="sm" c="dimmed">Deskripsi Singkat</Text>
{data?.deskripsiSingkat ? (
<Text fz="md" dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }} />
) : (
<Text fz="sm" c="dimmed">Belum ada deskripsi singkat</Text>
)}
</Stack>
<Stack gap={4}>
<Text fz="sm" c="dimmed">Deskripsi Lengkap</Text>
{data?.deskripsi ? (
<Text fz="md" dangerouslySetInnerHTML={{ __html: data.deskripsi }} />
) : (
<Text fz="sm" c="dimmed">Belum ada deskripsi</Text>
)}
</Stack>
<Stack gap={4}>
<Text fz="sm" c="dimmed">Video</Text>
{data?.linkVideo ? (
<Box
component="iframe"
src={convertToEmbedUrl(data.linkVideo)}
width="100%"
h={{ base: 320, md: 450 }}
allowFullScreen
style={{ borderRadius: 12, border: 'none' }}
/>
) : (
<Text fz="sm" c="dimmed">Belum ada video</Text>
)}
</Stack>
</Stack>
</Paper>
</Stack>
</Card>
</Box>
);
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch {
return youtubeUrl;
}
}
}
export default Page;
export default DetailPencegahanKriminalitas;

View File

@@ -1,11 +1,137 @@
import React from 'react';
'use client'
import {
Box,
Card,
Center,
Group,
Pagination,
Skeleton,
Stack,
Text,
Title,
Tooltip,
Button,
Paper,
} from '@mantine/core';
import { IconSearch, IconArrowRight } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
function PencegahanKriminalitas() {
const [search, setSearch] = useState("");
function Page() {
return (
<div>
Page
</div>
<Box>
<HeaderSearch
title="Program Pencegahan Kriminalitas"
placeholder="Cari program atau deskripsi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPencegahanKriminalitas search={search} />
</Box>
);
}
export default Page;
function ListPencegahanKriminalitas({ search }: { search: string }) {
const kriminalitasState = useProxy(pencegahanKriminalitasState);
const router = useRouter();
const { data, page, totalPages, loading, load } = kriminalitasState.findMany;
useShallowEffect(() => {
load(page, 6, search);
}, [page, search]);
if (loading || !data) {
return (
<Stack py="lg">
<Skeleton height={300} radius="lg" />
<Skeleton height={300} radius="lg" />
</Stack>
);
}
return (
<Box py="lg">
<Stack>
{data.length > 0 ? (
data.map((item) => (
<Card
key={item.id}
withBorder
radius="lg"
shadow="md"
p="lg"
style={{
background: 'linear-gradient(135deg, #ffffff 0%, #f9fbff 100%)',
}}
>
<Stack gap="xs">
<Title order={4} c="blue" style={{ fontWeight: 600 }}>
{item.judul}
</Title>
<Text
fz="sm"
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }}
/>
<Group justify="flex-end" mt="sm">
<Tooltip label="Lihat detail program" withArrow>
<Button
size="sm"
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
rightSection={<IconArrowRight size={18} />}
onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)}
>
Lihat Detail
</Button>
</Tooltip>
</Group>
</Stack>
</Card>
))
) : (
<Paper withBorder radius="lg" p="xl" shadow="sm">
<Center>
<Stack align="center" gap="xs">
<Text fz="lg" fw={500} c="dimmed">
Belum ada program pencegahan kriminalitas
</Text>
<Text fz="sm" c="dimmed">
Program akan ditampilkan di sini ketika tersedia
</Text>
</Stack>
</Center>
</Paper>
)}
</Stack>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 6, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="xl"
color="blue"
radius="lg"
/>
</Center>
)}
</Box>
);
}
export default PencegahanKriminalitas;

View File

@@ -3,55 +3,23 @@ import jadwalkegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_k
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Combobox,
ComboboxChevron,
ComboboxOption,
ComboboxOptions,
ComboboxTarget,
Divider,
Group,
InputBase,
InputPlaceholder,
Paper,
Skeleton,
Stack,
Text,
Textarea,
TextInput,
useCombobox
Text
} from '@mantine/core';
import { DateInput } from '@mantine/dates';
import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconDownload, IconPhone, IconMail, IconUser } from '@tabler/icons-react';
import { IconDownload, IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import CreatePendaftaran from '../create/page';
const layanan = [
'Penimbangan',
'Imunisasi',
'Vitamin A',
'Konsultasi Gizi',
'Pemeriksaan Kesehatan'
];
function Page() {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
onDropdownOpen: (eventSource) => {
if (eventSource === 'keyboard') {
combobox.selectActiveOption();
} else {
combobox.updateSelectedOptionIndex('active');
}
},
});
const [value, setValue] = useState<string | null>('');
const [dateInputOpened, setDateInputOpened] = useState(false);
const params = useParams();
const state = useProxy(jadwalkegiatanState);
@@ -59,20 +27,7 @@ function Page() {
state.findUnique.load(params?.id as string);
}, []);
const options = layanan.map((item) => (
<ComboboxOption value={item} key={item} active={item === value}>
<Group gap="xs">
{item === value && <IconUser size={14} stroke={2} />}
<span>{item}</span>
</Group>
</ComboboxOption>
));
const pickerControl = (
<ActionIcon onClick={() => setDateInputOpened(true)} variant="light" color="blue">
<IconCalendar size={18} />
</ActionIcon>
);
if (!state.findUnique.data) {
return (
@@ -133,56 +88,7 @@ function Page() {
<Text ta="justify" fz="md" dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Formulir Pendaftaran</Text>
<Divider />
<Stack gap="md">
<TextInput label="Nama Balita" placeholder="Masukkan nama balita" size="md" />
<DateInput
label="Tanggal Lahir"
placeholder="dd/mm/yyyy"
size="md"
w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}
popoverProps={{ opened: dateInputOpened, onChange: setDateInputOpened }}
rightSection={pickerControl}
/>
<TextInput label="Nama Orang Tua / Wali" placeholder="Masukkan nama orang tua / wali" size="md" />
<TextInput label="Nomor Telepon" placeholder="Masukkan nomor telepon" size="md" />
<TextInput label="Alamat" placeholder="Masukkan alamat lengkap" size="md" />
<Box w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}>
<Text fz="sm" fw="bold" pb={4}>Pilih Layanan</Text>
<Combobox
store={combobox}
resetSelectionOnOptionHover
withinPortal={false}
onOptionSubmit={(val) => {
setValue(val);
combobox.updateSelectedOptionIndex('active');
}}
>
<ComboboxTarget targetType="button">
<InputBase
component="button"
type="button"
pointer
rightSection={<ComboboxChevron />}
rightSectionPointerEvents="none"
onClick={() => combobox.toggleDropdown()}
>
{value || <InputPlaceholder>Pilih layanan</InputPlaceholder>}
</InputBase>
</ComboboxTarget>
<Combobox.Dropdown>
<ComboboxOptions>{options}</ComboboxOptions>
</Combobox.Dropdown>
</Combobox>
</Box>
<Textarea label="Catatan Khusus (Opsional)" placeholder="Masukkan catatan jika ada" size="md" />
<Button size="md" radius="lg" bg={colors['blue-button']}>
Daftar Sekarang
</Button>
</Stack>
</Stack>
<CreatePendaftaran />
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
<Stack gap="xs">

View File

@@ -0,0 +1,90 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pendaftaranJadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/pendafataranJadwalKegiatan';
import colors from '@/con/colors';
import { Button, Divider, Stack, Text, Textarea, TextInput } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreatePendaftaran() {
const stateCreate = useProxy(pendaftaranJadwalKegiatanState);
useEffect(() => {
stateCreate.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
name: '',
alamat: '',
tanggal: '',
namaOrangtua: '',
nomor: '',
catatan: '',
};
};
const handleSubmit = async () => {
await stateCreate.create.submit();
resetForm();
};
return (
<Stack gap="sm">
<Text fz="lg" fw="bold">Formulir Pendaftaran</Text>
<Divider />
<Stack gap="md">
<TextInput
label="Nama Balita"
placeholder="Masukkan nama balita"
size="md"
value={stateCreate.create.form.name}
onChange={(e) => stateCreate.create.form.name = e.target.value}
/>
<TextInput
type='date'
label="Tanggal Lahir"
placeholder="dd/mm/yyyy"
size="md"
w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}
value={stateCreate.create.form.tanggal}
onChange={(e) => stateCreate.create.form.tanggal = e.target.value}
/>
<TextInput
label="Nama Orang Tua / Wali"
placeholder="Masukkan nama orang tua / wali"
size="md"
value={stateCreate.create.form.namaOrangtua}
onChange={(e) => stateCreate.create.form.namaOrangtua = e.target.value}
/>
<TextInput
label="Nomor Telepon"
placeholder="Masukkan nomor telepon"
size="md"
value={stateCreate.create.form.nomor}
onChange={(e) => stateCreate.create.form.nomor = e.target.value}
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
size="md"
value={stateCreate.create.form.alamat}
onChange={(e) => stateCreate.create.form.alamat = e.target.value}
/>
<Textarea
label="Catatan Khusus (Opsional)"
placeholder="Masukkan catatan jika ada"
size="md"
value={stateCreate.create.form.catatan}
onChange={(e) => stateCreate.create.form.catatan = e.target.value}
/>
<Button size="md" radius="lg" bg={colors['blue-button']} onClick={handleSubmit}>
Daftar Sekarang
</Button>
</Stack>
</Stack>
);
}
export default CreatePendaftaran;