API & UI Menu Inovasi & Submenu Layan Online Desa 2 tabs

This commit is contained in:
2025-07-16 00:20:55 +08:00
parent e8ad74d118
commit 55433128a9
45 changed files with 2566 additions and 399 deletions

View File

@@ -275,8 +275,7 @@ const kategoriProduk = proxy({
data: null as Array<{
id: string;
nama: string;
}>
| null,
}> | null,
async load() {
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
if (res.status === 200) {
@@ -335,125 +334,135 @@ const kategoriProduk = proxy({
}
},
},
edit: {
id: "",
form: { ...kategoriProdukDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
edit: {
id: "",
form: { ...kategoriProdukDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
try {
const response = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
method: "GET",
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriProduk.edit.loading = true;
const response = await fetch(
`/api/ekonomi/kategoriproduk/${kategoriProduk.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
body: JSON.stringify({
nama: kategoriProduk.edit.form.nama,
}),
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
kategoriProduk.edit.loading = true;
const response = await fetch(
`/api/ekonomi/kategoriproduk/${kategoriProduk.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: kategoriProduk.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error('Update failed with status:', response.status, 'Response:', result);
throw new Error(
result?.message || `Gagal mengupdate kategori produk (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui kategori produk");
await kategoriProduk.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kategori produk");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error('Error response text:', text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error('Error parsing response as text:', textError);
console.error('Original error:', error);
throw new Error('Gagal memproses respons dari server');
}
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate kategori produk (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui kategori produk"
);
await kategoriProduk.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate kategori produk"
);
}
} catch (error) {
console.error("Error updating kategori produk:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori produk"
);
return false;
} finally {
kategoriProduk.edit.loading = false;
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
},
reset() {
kategoriProduk.edit.id = "";
kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
},
} catch (error) {
console.error("Error updating kategori produk:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori produk"
);
return false;
} finally {
kategoriProduk.edit.loading = false;
}
},
reset() {
kategoriProduk.edit.id = "";
kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
},
},
});
const pasarDesaState = proxy({
pasarDesa,
kategoriProduk
kategoriProduk,
});
export default pasarDesaState;

View File

@@ -0,0 +1,123 @@
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).max(50),
deskripsi: z.string().min(1).max(5000),
alamat: z.string().min(1).max(5000),
namaIde: z.string().min(1).max(5000),
masalah: z.string().min(1).max(5000),
benefit: z.string().min(1).max(5000),
});
const defaultForm = {
name: "",
deskripsi: "",
alamat: "",
namaIde: "",
masalah: "",
benefit: "",
};
const ajukanIdeInovatifState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(ajukanIdeInovatifState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanIdeInovatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.ajukanideinovatif["create"].post(
ajukanIdeInovatifState.create.form
);
if (res.status === 200) {
ajukanIdeInovatifState.findMany.load();
return toast.success("Ajukan Ide Inovatif berhasil di kirim");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
ajukanIdeInovatifState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.AjukanIdeInovatifGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get();
if (res.status === 200) {
ajukanIdeInovatifState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.AjukanIdeInovatifGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/ajukanideinovatif/${id}`);
if (res.ok) {
const data = await res.json();
ajukanIdeInovatifState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
ajukanIdeInovatifState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading ajukan ide inovatif:", error);
ajukanIdeInovatifState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
ajukanIdeInovatifState.delete.loading = true;
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");
await ajukanIdeInovatifState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");
}
} catch (error) {
console.log((error as Error).message);
toast.error("Terjadi kesalahan saat menghapus ajukan ide inovatif");
} finally {
ajukanIdeInovatifState.delete.loading = false;
}
},
},
});
export default ajukanIdeInovatifState;

View File

@@ -0,0 +1,403 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= ADMINISTRASI ONLINE ========================================= //
const templateAdministrasiOnlineForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor telepon minimal 1 karakter"),
jenisLayananId: z.string().min(1, "Jenis layanan minimal 1 karakter"),
});
const defaultAdministrasiOnlineForm = {
name: "",
alamat: "",
nomorTelepon: "",
jenisLayananId: "",
};
const administrasiOnline = proxy({
create: {
form: { ...defaultAdministrasiOnlineForm },
loading: false,
async create() {
const cek = templateAdministrasiOnlineForm.safeParse(
administrasiOnline.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
administrasiOnline.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
"create"
].post(administrasiOnline.create.form);
if (res.status === 200) {
administrasiOnline.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
administrasiOnline.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page;
try {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
"find-many"
].get({
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) {
administrasiOnline.findMany.data = res.data.data ?? [];
administrasiOnline.findMany.totalPages = res.data.totalPages ?? 1;
}
} catch (err) {
console.error("Gagal fetch administrasi online paginated:", err);
} finally {
administrasiOnline.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.AdministrasiOnlineGetPayload<{
include: {
jenisLayanan: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/${id}`
);
if (res.ok) {
const data = await res.json();
administrasiOnline.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch administrasi online:", res.statusText);
administrasiOnline.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching administrasi online:", error);
administrasiOnline.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
administrasiOnline.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Administrasi online berhasil dihapus"
);
await administrasiOnline.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus administrasi online");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus administrasi online");
} finally {
administrasiOnline.delete.loading = false;
}
},
},
});
// ========================================= JENIS LAYANAN ========================================= //
const templateJenisLayananForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
});
const defaultJenisLayananForm = {
nama: "",
deskripsi: "",
};
const jenisLayanan = proxy({
create: {
form: { ...defaultJenisLayananForm },
loading: false,
async create() {
const cek = templateJenisLayananForm.safeParse(jenisLayanan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jenisLayanan.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"create"
].post(jenisLayanan.create.form);
if (res.status === 200) {
jenisLayanan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
jenisLayanan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
deskripsi: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
"find-many"
].get();
if (res.status === 200) {
jenisLayanan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.JenisLayananGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${id}`
);
if (res.ok) {
const data = await res.json();
jenisLayanan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenisLayanan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
jenisLayanan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenisLayanan.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Jenis layanan berhasil dihapus");
await jenisLayanan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus jenis layanan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenis layanan");
} finally {
jenisLayanan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultJenisLayananForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
deskripsi: data.deskripsi,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading jenis layanan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateJenisLayananForm.safeParse(jenisLayanan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
jenisLayanan.edit.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${jenisLayanan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: jenisLayanan.edit.form.nama,
deskripsi: jenisLayanan.edit.form.deskripsi,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate jenis layanan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui jenis layanan"
);
await jenisLayanan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate jenis layanan");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating jenis layanan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate jenis layanan"
);
return false;
} finally {
jenisLayanan.edit.loading = false;
}
},
reset() {
jenisLayanan.edit.id = "";
jenisLayanan.edit.form = { ...defaultJenisLayananForm };
},
},
});
const layananonlineDesa = proxy({
administrasiOnline,
jenisLayanan,
});
export default layananonlineDesa;

View File

@@ -0,0 +1,108 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif';
function DetailAjukanIdeInofativDesa() {
const state = useProxy(ajukanIdeInovatifState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
useShallowEffect(() => {
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")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<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);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
</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}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus ajukan ide inovatif ini?'
/>
</Box>
);
}
export default DetailAjukanIdeInofativDesa;

View File

@@ -1,59 +1,91 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import React from 'react';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, 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 ajukanIdeInovatifState from '../../_state/inovasi/ajukan-ide-inovatif';
function AjukanIdeInofativ() {
function AjukanIdeInovatif() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Ajukan Ide Inovatif'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAjukanIdeInovatif />
<ListAjukanIdeInovatif search={search} />
</Box>
);
}
function ListAjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState)
const router = useRouter()
useShallowEffect(() => {
state.findMany.load()
}, [])
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)
);
});
if (!state.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>List Ajukan Ide Inovatif</Title>
<Box>
<Table striped withRowBorders withColumnBorders withTableBorder>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Ide Inovatif</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Masalah yang ingin diatasi</TableTh>
<TableTh>Benefit</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>1</TableTd>
<TableTd>nama</TableTd>
<TableTd>alamat</TableTd>
<TableTd>ide inovatif</TableTd>
<TableTd>deskripsi</TableTd>
<TableTd>masalah</TableTd>
<TableTd>benefit</TableTd>
</TableTr>
</TableTbody>
</Table> </Box>
</Stack>
<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>
</Box>
)
);
}
export default AjukanIdeInofativ;
export default AjukanIdeInovatif;

View File

@@ -1,42 +0,0 @@
'use client'
import colors from "@/con/colors";
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
// import { KeamananEditor } from "../../../keamanan/_com/keamananEditor";
export default function EditLayananOnlineDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Layanan Online Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Layanan Online Desa</Text>}
placeholder='Masukkan nama LayananOnlineDesa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Layanan Online Desa</Text>
{/* <KeamananEditor
showSubmit={false}
/> */}
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailLayananOnlineDesa() {
const router = useRouter();
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 Layanan Online Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Layanan Online Desa</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/layanan-online-desa/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailLayananOnlineDesa;

View File

@@ -0,0 +1,73 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Administrasi Online",
value: "administrasionline",
href: "/admin/inovasi/layanan-online-desa/administrasi-online"
},
{
label: "Jenis Layanan",
value: "jenislayanan",
href: "/admin/inovasi/layanan-online-desa/jenis-layanan"
},
{
label: "Pengaduan Masyarakat",
value: "pengaduanmasyarakat",
href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat"
},
{
label: "Informasi Desa",
value: "informasidesa",
href: "/admin/inovasi/layanan-online-desa/informasi-desa"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const 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>
<Title order={3}>Layanan Online Desa</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>
{children}
</Stack>
);
}
export default LayoutTabsLayananOnlineDesa;

View File

@@ -0,0 +1,104 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailAdministrasiOnline() {
const beritaState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
beritaState.administrasiOnline.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
beritaState.administrasiOnline.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/administrasi-online")
}
}
if (!beritaState.administrasiOnline.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Administrasi Online</Text>
<Button
onClick={() => {
if (beritaState.administrasiOnline.findUnique.data) {
setSelectedId(beritaState.administrasiOnline.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={beritaState.administrasiOnline.delete.loading || !beritaState.administrasiOnline.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{beritaState.administrasiOnline.findUnique.data ? (
<Paper key={beritaState.administrasiOnline.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{beritaState.administrasiOnline.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"}>{beritaState.administrasiOnline.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{beritaState.administrasiOnline.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Layanan</Text>
<Text fz={"lg"} >{beritaState.administrasiOnline.findUnique.data?.jenisLayanan?.nama}</Text>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
/>
</Box>
);
}
export default DetailAdministrasiOnline;

View File

@@ -0,0 +1,99 @@
'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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function AdministrasiOnline() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Administrasi Online'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAdministrasiOnline search={search} />
</Box>
);
}
function ListAdministrasiOnline({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.administrasiOnline)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return <Skeleton h={500} />;
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Administrasi Online</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Layanan</TableTh>
<TableTh>Alamat</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.alamat}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default AdministrasiOnline;

View File

@@ -1,44 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function CreateLayananOnlineDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Layanan Online Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Layanan Online Desa</Text>}
placeholder='Masukkan nama LayananOnlineDesa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Layanan Online Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateLayananOnlineDesa;

View File

@@ -0,0 +1,92 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditJenisLayanan() {
const state = useProxy(layananonlineDesa.jenisLayanan)
const router = useRouter()
const params = useParams()
const [formData, setFormData] = useState({
nama: state.edit.form.nama,
deskripsi: state.edit.form.deskripsi,
})
useEffect(() => {
const loadJenisLayanan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.edit.load(id);
if (data) {
setFormData({
nama: data.nama,
deskripsi: data.deskripsi,
});
}
} catch (error) {
console.error("Error loading jenis layanan:", error);
toast.error("Gagal memuat data jenis layanan");
}
};
loadJenisLayanan();
}, [params?.id]);
const handleSubmit = async () => {
try {
state.edit.form = {
...state.edit.form,
nama: formData.nama,
deskripsi: formData.deskripsi,
}
await state.edit.update()
toast.success("Jenis layanan berhasil diperbarui!")
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
} catch (error) {
console.error("Error updating jenis layanan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
}
}
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}>Edit Jenis Layanan</Title>
<TextInput
value={formData.nama}
onChange={(val) => {
setFormData({ ...formData, nama: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Jenis Layanan</Text>}
placeholder="masukkan nama jenis layanan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditJenisLayanan;

View File

@@ -0,0 +1,103 @@
'use client'
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 { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailJenisLayanan() {
const state = useProxy(layananonlineDesa.jenisLayanan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail 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}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<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"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus jenis layanan ini?'
/>
</Box>
);
}
export default DetailJenisLayanan;

View File

@@ -0,0 +1,70 @@
/* eslint-disable react-hooks/exhaustive-deps */
'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 { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJenisLayanan() {
const router = useRouter();
const statePasar = useProxy(layananonlineDesa.jenisLayanan)
useEffect(() => {
statePasar.findMany.load();
}, []);
const resetForm = () => {
statePasar.create.form = {
nama: "",
deskripsi: "",
};
}
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
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} />
</Button>
</Box>
<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;
}}
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>
</Box>
);
}
export default CreateJenisLayanan;

View File

@@ -0,0 +1,88 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, 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 layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisLayanan() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Jenis Layanan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListJenisLayanan search={search} />
</Box>
);
}
function ListJenisLayanan({ search }: { search: string }) {
const stateList = useProxy(layananonlineDesa.jenisLayanan)
const router = useRouter()
useShallowEffect(() => {
stateList.findMany.load()
}, [])
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!stateList.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenis Layanan'
href='/admin/inovasi/layanan-online-desa/jenis-layanan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Layanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.deskripsi}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default JenisLayanan;

View File

@@ -0,0 +1,12 @@
'use client'
import LayoutTabsLayananOnlineDesa from "./_lib/layoutTabs";
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsLayananOnlineDesa>
{children}
</LayoutTabsLayananOnlineDesa>
);
}
export default Layout;

View File

@@ -1,56 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function LayananOnlineDesa() {
return (
<Box>
<HeaderSearch
title='Layanan Online Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListLayananOnlineDesa/>
</Box>
);
}
function ListLayananOnlineDesa() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Layanan Online Desa'
href='/admin/inovasi/layanan-online-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Layanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Layanan Online Desa 1</TableTd>
<TableTd>Deskripsi Layanan Online Desa 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/layanan-online-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default LayananOnlineDesa;

View File

@@ -276,7 +276,7 @@ export const navBar = [
{
id: "Inovasi_2",
name: "Layanan Online Desa",
path: "/admin/inovasi/layanan-online-desa"
path: "/admin/inovasi/layanan-online-desa/administrasi-online"
},
{
id: "Inovasi_3",

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreateAjukanIdeInovatif = Prisma.AjukanIdeInovatifGetPayload<{
select: {
name: true;
deskripsi: true;
alamat: true;
namaIde: true;
masalah: true;
benefit: true;
}
}>
export default async function ajukanIdeInovatifCreate(context: Context){
const body = context.body as FormCreateAjukanIdeInovatif;
await prisma.ajukanIdeInovatif.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
alamat: body.alamat,
namaIde: body.namaIde,
masalah: body.masalah,
benefit: body.benefit,
}
})
return {
success: true,
message: "Success create ajukan ide inovatif",
data: {
...body,
}
}
}

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function ajukanIdeInovatifDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const ajukanIdeInovatif = await prisma.ajukanIdeInovatif.findUnique({
where: { id },
});
if (!ajukanIdeInovatif) {
return {
status: 404,
body: "Ajukan ide inovatif tidak ditemukan",
};
}
await prisma.ajukanIdeInovatif.delete({
where: { id },
});
return {
success: true,
message: "Ajukan ide inovatif berhasil dihapus",
status: 200,
};
}

View File

@@ -0,0 +1,19 @@
import prisma from "@/lib/prisma";
export default async function ajukanIdeInovatifFindMany() {
try {
const data = await prisma.ajukanIdeInovatif.findMany({});
return {
success: true,
message: "Success fetch ajukan ide inovatif",
data,
};
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch ajukan ide inovatif",
};
}
}

View File

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

View File

@@ -0,0 +1,27 @@
import Elysia, { t } from "elysia";
import ajukanIdeInovatifCreate from "./create";
import ajukanIdeInovatifFindMany from "./findMany";
import ajukanIdeInovatifFindUnique from "./findUnique";
import ajukanIdeInovatifDelete from "./del";
const AjukanIdeInovatif = new Elysia({
prefix: "/ajukanideinovatif",
tags: ["Inovasi/AjukanIde"],
})
.post("/create", ajukanIdeInovatifCreate, {
body: t.Object({
name: t.String(),
deskripsi: t.String(),
alamat: t.String(),
namaIde: t.String(),
masalah: t.String(),
benefit: t.String(),
}),
})
.get("/find-many", ajukanIdeInovatifFindMany)
.get("/:id", async (context) => {
const response = await ajukanIdeInovatifFindUnique(context.request);
return response;
})
.delete("/del/:id", ajukanIdeInovatifDelete);
export default AjukanIdeInovatif;

View File

@@ -3,6 +3,8 @@ import DesaDigital from "./desa-digital";
import ProgramKreatif from "./program-kreatif";
import KolaborasiInovasi from "./kolaborasi-inovasi";
import InfoTekno from "./info-teknologi";
import AjukanIdeInovatif from "./ajukan-ide-inovatif";
import LayananOnlineDesa from "./layanan-online-desa";
const Inovasi = new Elysia({
prefix: "/api/inovasi",
@@ -10,7 +12,9 @@ const Inovasi = new Elysia({
})
.use(DesaDigital)
.use(ProgramKreatif)
.use(KolaborasiInovasi)
.use(KolaborasiInovasi)
.use(InfoTekno)
.use(AjukanIdeInovatif)
.use(LayananOnlineDesa)
export default Inovasi;

View File

@@ -0,0 +1,43 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
alamat: string;
nomorTelepon: string;
jenisLayananId: string;
};
export default async function administrasiOnlineCreate(context: Context) {
const body = context.body as FormCreate;
if (!body.jenisLayananId) {
throw new Error("jenisLayananId wajib diisi");
}
try {
// Create langsung data AdministrasiOnline
const result = await prisma.administrasiOnline.create({
data: {
name: body.name,
alamat: body.alamat,
nomorTelepon: body.nomorTelepon,
jenisLayananId: body.jenisLayananId, // relasi ke JenisLayanan
},
include: {
jenisLayanan: true, // Include data relasi
},
});
return {
success: true,
message: "Berhasil membuat administrasi online",
data: result,
};
} catch (error) {
console.error("Error creating administrasi online:", error);
throw new Error(
"Gagal membuat administrasi online: " + (error as Error).message
);
}
}

View File

@@ -0,0 +1,21 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function administrasiOnlineDelete(context: Context) {
const { params } = context;
const id = params?.id as string;
if (!id) {
throw new Error("ID tidak ditemukan dalam parameter");
}
const deleted = await prisma.administrasiOnline.delete({
where: { id },
});
return {
success: true,
message: "Berhasil menghapus administrasi online",
data: deleted,
};
}

View File

@@ -0,0 +1,43 @@
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function administrasiOnlineFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const skip = (page - 1) * limit;
try {
const [data, total] = await Promise.all([
prisma.administrasiOnline.findMany({
where: { isActive: true },
include: {
jenisLayanan: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.administrasiOnline.count({
where: { isActive: true }
})
]);
return {
success: true,
message: "Success fetch administrasi online with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch administrasi online with pagination",
};
}
}
export default administrasiOnlineFindMany;

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function administrasiOnlineFindUnique(context: Context) {
const { params } = context;
const id = params?.id as string;
if (!id) {
throw new Error("ID tidak ditemukan dalam parameter");
}
const data = await prisma.administrasiOnline.findUnique({
where: { id },
include: {
jenisLayanan: true,
},
});
if (!data) {
throw new Error("Administrasi online tidak ditemukan");
}
return {
success: true,
message: "Data administrasi online ditemukan",
data,
};
}

View File

@@ -0,0 +1,26 @@
import Elysia from "elysia";
import JenisLayanan from "./jenis-layanan";
import administrasiOnlineFindMany from "./findMany";
import administrasiOnlineFindUnique from "./findUnique";
import administrasiOnlineDelete from "./del";
import administrasiOnlineCreate from "./create";
import { t } from "elysia";
const AdministrasiOnline = new Elysia({
prefix: "/administrasionline",
tags: ["Inovasi/Layanan Online Desa/Administrasi Online"],
})
.get("/find-many", administrasiOnlineFindMany)
.get("/:id", administrasiOnlineFindUnique)
.delete("/del/:id", administrasiOnlineDelete)
.post("/create", administrasiOnlineCreate, {
body: t.Object({
name: t.String(),
alamat: t.String(),
nomorTelepon: t.String(),
jenisLayananId: t.String(),
}),
})
.use(JenisLayanan);
export default AdministrasiOnline;

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function jenisLayananCreate(context: Context) {
const body = context.body as {nama: string, deskripsi: string};
if (!body.nama) {
return {
success: false,
message: "Nama is required",
};
}
const jenisLayanan = await prisma.jenisLayanan.create({
data: {
nama: body.nama,
deskripsi: body.deskripsi,
},
});
return {
success: true,
message: "Success create jenis layanan",
data: jenisLayanan
};
}

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const jenisLayananDelete = async (context: Context) => {
const id = context.params.id;
if (!id) {
return {
success: false,
message: "ID is required",
}
}
const jenisLayanan = await prisma.jenisLayanan.delete({
where: {
id: id,
},
})
if(!jenisLayanan) {
return {
success: false,
message: "Jenis layanan tidak ditemukan",
}
}
return {
success: true,
message: "Success delete jenis layanan",
data: jenisLayanan,
}
}
export default jenisLayananDelete

View File

@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
export default async function jenisLayananFindMany() {
const data = await prisma.jenisLayanan.findMany();
return {
success: true,
data: data.map((item: any) => {
return {
id: item.id,
nama: item.nama,
deskripsi: item.deskripsi,
}
}),
};
}

View File

@@ -0,0 +1,47 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export default async function jenisLayananFindUnique(context: Context) {
const url = new URL(context.request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.jenisLayanan.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Jenis layanan tidak ditemukan",
}
}
return {
success: true,
message: "Success find jenis layanan",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil jenis layanan: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,32 @@
import Elysia from "elysia";
import jenisLayananFindMany from "./findMany";
import jenisLayananFindUnique from "./findUnique";
import jenisLayananDelete from "./del";
import jenisLayananCreate from "./create";
import jenisLayananUpdate from "./updt";
import { t } from "elysia";
const JenisLayanan = new Elysia({
prefix: "/jenislayanan",
tags: ["Inovasi/Jenis Layanan"],
})
.get("/find-many", jenisLayananFindMany)
.get("/:id", async (context) => {
const response = await jenisLayananFindUnique(context);
return response;
})
.delete("/del/:id", jenisLayananDelete)
.post("/create", jenisLayananCreate, {
body: t.Object({
nama: t.String(),
deskripsi: t.String(),
}),
})
.put("/:id", jenisLayananUpdate, {
body: t.Object({
nama: t.String(),
deskripsi: t.String(),
}),
});
export default JenisLayanan;

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function jenisLayananUpdate(context: Context) {
const body = context.body as { nama: string, deskripsi: string };
const id = context.params?.id as string;
// Validasi ID dan nama
if (!id) {
return {
success: false,
message: "ID is required",
};
}
if (!body.nama) {
return {
success: false,
message: "Nama is required",
};
}
try {
const jenisLayanan = await prisma.jenisLayanan.update({
where: { id },
data: {
nama: body.nama,
deskripsi: body.deskripsi,
},
});
return {
success: true,
message: "Success update jenis layanan",
data: jenisLayanan,
};
} catch (error) {
console.error("Update error:", error);
return {
success: false,
message: "Gagal update jenis layanan",
error: error instanceof Error ? error.message : String(error),
};
}
}

View File

@@ -0,0 +1,13 @@
import Elysia from "elysia";
import AdministrasiOnline from "./administrasi-online";
const LayananOnlineDesa = new Elysia({
prefix: "/layananonlinedesa",
tags: ["Inovasi/Layanan Online Desa"],
}).use(AdministrasiOnline);
export default LayananOnlineDesa;

View File

@@ -1,10 +1,40 @@
'use client'
import ajukanIdeInovatifState from '@/app/admin/(dashboard)/_state/inovasi/ajukan-ide-inovatif';
import colors from '@/con/colors';
import { Stack, Box, Text, SimpleGrid, Paper, List, ListItem, Flex, ActionIcon } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { ActionIcon, Box, Button, Flex, List, ListItem, Modal, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconArrowRight, IconBulbFilled } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
function Page() {
const [opened, { open, close }] = useDisclosure(false);
const ideInovatif = useProxy(ajukanIdeInovatifState)
const resetForm = () => {
// Reset state di valtio
ideInovatif.create.form = {
name: "",
deskripsi: "",
alamat: "",
namaIde: "",
masalah: "",
benefit: "",
};
// Reset state lokal
};
const handleSubmit = async () => {
// Submit data berita
await ideInovatif.create.create();
// Reset form setelah submit
resetForm();
close();
};
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
@@ -40,7 +70,7 @@ function Page() {
<IconArrowRight size={30} color={colors['blue-button']} />
</Box>
<Box px={{ base: 5, md: 10 }} py={5}>
<ActionIcon variant="transparent" size={150}>
<ActionIcon variant="transparent" size={150} onClick={open}>
<IconBulbFilled size={150} color={colors['blue-button']} />
</ActionIcon>
</Box>
@@ -49,6 +79,65 @@ function Page() {
</SimpleGrid>
</Stack>
</Box>
<Modal
opened={opened}
onClose={close}
radius={0}
transitionProps={{ transition: 'fade', duration: 200 }}
>
<Paper p={"md"} withBorder>
<Stack gap={"xs"}>
<Title order={3}>Ajukan Ide Inovatif</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
placeholder="masukkan nama"
onChange={(val) => {
ideInovatif.create.form.name = val.target.value
}}
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>}
placeholder="masukkan alamat"
onChange={(val) => {
ideInovatif.create.form.alamat = val.target.value
}}
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Ide</Text>}
placeholder="masukkan nama ide"
onChange={(val) => {
ideInovatif.create.form.namaIde = val.target.value
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={ideInovatif.create.form.deskripsi}
onChange={(htmlContent) => {
ideInovatif.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Masalah</Text>}
placeholder="masukkan masalah"
onChange={(val) => {
ideInovatif.create.form.masalah = val.target.value
}}
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Benefit</Text>}
placeholder="masukkan benefit"
onChange={(val) => {
ideInovatif.create.form.benefit = val.target.value
}}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Modal>
</Stack>
);
}

View File

@@ -0,0 +1,117 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
import {
Box,
Button,
Modal,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconFileCheckFilled } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function AdministrasiOnline() {
const [opened, { open, close }] = useDisclosure(false);
const state = useProxy(layananonlineDesa);
useEffect(() => {
// ✅ Panggil load data jenis layanan dari backend
if (!state.jenisLayanan.findMany.data) {
state.jenisLayanan.findMany.load();
}
}, []);
const resetForm = () => {
state.administrasiOnline.create.form = {
name: '',
alamat: '',
nomorTelepon: '',
jenisLayananId: '',
};
};
const handleSubmit = async () => {
await state.administrasiOnline.create.create();
resetForm();
close(); // Tutup modal setelah submit
};
return (
<Box>
<Stack>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.8 }} onClick={open}>
<Paper p="xl" withBorder>
<Box>
<IconFileCheckFilled size={50} color={colors['blue-button']} />
</Box>
<Text fz="h3" fw="bold" c={colors['blue-button']}>
Administrasi Online
</Text>
<Text fz="lg" c="black">
Pengurusan surat dan dokumen secara digital tanpa perlu datang ke kantor desa
</Text>
</Paper>
</motion.div>
</Stack>
<Modal
opened={opened}
onClose={close}
radius={0}
transitionProps={{ transition: 'fade', duration: 200 }}
>
<Paper p="md" withBorder>
<Stack gap="xs">
<Title order={3}>Ajukan Administrasi Online</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
onChange={(val) => (state.administrasiOnline.create.form.name = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
onChange={(val) => (state.administrasiOnline.create.form.alamat = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor Telepon</Text>}
placeholder="masukkan nomor telepon"
onChange={(val) => (state.administrasiOnline.create.form.nomorTelepon = val.target.value)}
/>
<Select
value={state.administrasiOnline.create.form.jenisLayananId}
onChange={(val) => {
state.administrasiOnline.create.form.jenisLayananId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Jenis Layanan</Text>}
placeholder="Pilih kategori produk"
data={
state.jenisLayanan.findMany.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>
</Stack>
</Paper>
</Modal>
</Box>
);
}
export default AdministrasiOnline;

View File

@@ -0,0 +1,28 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Stack, Text } from '@mantine/core';
import { IconBell } from '@tabler/icons-react';
import { motion } from 'framer-motion';
function InformasiDesa() {
return (
<Box>
<Stack >
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
<Paper p={'xl'} >
<Box>
<IconBell size={50} color={colors['blue-button']} />,
</Box>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Informasi Desa</Text>
<Text fz={'lg'} c={'black'}>Akses berita dan pengumuman terbaru seputar kegiatan desa</Text>
</Paper>
</motion.div>
</Stack>
</Box>
);
}
export default InformasiDesa;

View File

@@ -1,28 +1,11 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { IconBell, IconFileCheckFilled, IconMessageCircleQuestion } from '@tabler/icons-react';
import { Box, SimpleGrid, Stack, Text } from '@mantine/core';
import BackButton from '../../desa/layanan/_com/BackButto';
import AdministrasiOnline from './administrasi-online/page';
import InformasiDesa from './informasi-desa/page';
import PengaduanMasyarakat from './pengaduan-masyarakat/page';
const data = [
{
id: 1,
icon: <IconFileCheckFilled size={50} color={colors['blue-button']} />,
judul: 'Administrasi Online',
deskripsi: 'Pengurusan surat dan dokumen secara digital tanpa perlu datang ke kantor desa'
},
{
id: 2,
icon: <IconMessageCircleQuestion size={50} color={colors['blue-button']} />,
judul: 'Pengaduan Masyarakat',
deskripsi: 'Sampaikan keluhan dan aspirasi Anda melalui platform digital kami'
},
{
id: 3,
icon: <IconBell size={50} color={colors['blue-button']} />,
judul: 'Informasi Desa',
deskripsi: 'Akses berita dan pengumuman terbaru seputar kegiatan desa'
},
]
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
@@ -37,30 +20,20 @@ function Page() {
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<SimpleGrid
pb={10}
cols={{
base: 1,
md: 1
}}
>
{data.map((v, k) => {
return (
<Stack key={k} >
<Paper p={'xl'} >
<Box>
{v.icon}
</Box>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
<Text fz={'lg'} c={'black'}>{v.deskripsi}</Text>
</Paper>
</Stack>
)
})}
<Stack gap={'lg'}>
<AdministrasiOnline />
<PengaduanMasyarakat />
<InformasiDesa />
</Stack>
</SimpleGrid>
</Stack>
</Box>
</Stack>
</Box >
</Stack >
);
}

View File

@@ -0,0 +1,28 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Stack, Text } from '@mantine/core';
import { IconMessageCircleQuestion } from '@tabler/icons-react';
import { motion } from 'framer-motion';
function PengaduanMasyarakat() {
return (
<Box>
<Stack >
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
<Paper p={'xl'} >
<Box>
<IconMessageCircleQuestion size={50} color={colors['blue-button']} />,
</Box>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Pengaduan Masyarakat</Text>
<Text fz={'lg'} c={'black'}>Sampaikan keluhan dan aspirasi Anda melalui platform digital kami</Text>
</Paper>
</motion.div>
</Stack>
</Box>
);
}
export default PengaduanMasyarakat;