Compare commits

...

5 Commits

Author SHA1 Message Date
f9bd2cea11 tambahan 2025-05-27 16:18:23 +08:00
5734e5d9a7 Selasa, 27 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu Inovasi
* Progress API ProfilePPID

Yang Akan Dikerjakan:
* API Menu Lain
* Tampilan UI Admin Di Menu Lingkungan
* Tampilan UI Admin Di Menu Pendidikan
2025-05-27 11:23:20 +08:00
3654629bde Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Akan Dikerjakan:
* API ProfilePPID
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 17:15:07 +08:00
02738104b5 Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, dan delete berita

Yang Akan Dikerjakan:
* API Di Menu Desa : Edit Berita
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 14:26:10 +08:00
92de697ae0 Nico-25 Mei 2025:
Membuat create berita dan gambar
Membuat fungsi tombol di mana bisa menghapus konten sesuai idnya
2025-05-25 11:33:50 +08:00
38 changed files with 1917 additions and 232 deletions

View File

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -0,0 +1,36 @@
// components/modal/ModalKonfirmasiHapus.tsx
import colors from "@/con/colors"
import { Modal, Text, Button, Flex } from "@mantine/core"
interface ModalKonfirmasiHapusProps {
opened: boolean
loading?: boolean
onClose: () => void
onConfirm: () => void
text: string
}
export function ModalKonfirmasiHapus({
opened,
loading = false,
onClose,
onConfirm,
text,
}: ModalKonfirmasiHapusProps) {
return (
<Modal
opened={opened}
onClose={onClose}
title="Konfirmasi Hapus"
centered
>
<Text mb="md">{text}</Text>
<Flex justify="flex-end" gap="sm">
<Button style={{color: "white"}} bg={colors['blue-button']} variant="default" onClick={onClose}>Batal</Button>
<Button color="red" onClick={onConfirm} loading={loading}>
Yakin Hapus
</Button>
</Flex>
</Modal>
)
}

View File

@@ -1,10 +1,11 @@
/* 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";
// 1. Schema validasi dengan Zod
const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
@@ -13,11 +14,19 @@ const templateForm = z.object({
imageId: z.string().nonempty(),
});
// 2. Default value form berita (hindari uncontrolled input)
const defaultForm = {
judul: "",
deskripsi: "",
imageId: "",
content: "",
kategoriBeritaId: "",
};
// 3. Kategori proxy
const category = proxy({
findMany: {
data: null as
| null
| Prisma.KategoriBeritaGetPayload<{ omit: { isActive: true } }>[],
data: [] as Prisma.KategoriBeritaGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.desa.berita.category["find-many"].get();
if (res.status === 200) {
@@ -27,19 +36,10 @@ const category = proxy({
},
});
type BeritaForm = Prisma.BeritaGetPayload<{
select: {
judul: true;
deskripsi: true;
imageId: true;
content: true;
kategoriBeritaId: true;
};
}>;
// 4. Berita proxy
const berita = proxy({
create: {
form: {} as BeritaForm,
form: { ...defaultForm }, // ✅ ini kunci fix-nya
loading: false,
async create() {
const cek = templateForm.safeParse(berita.create.form);
@@ -49,6 +49,7 @@ const berita = proxy({
.join("\n")}] required`;
return toast.error(err);
}
try {
berita.create.loading = true;
const res = await ApiFetch.api.desa.berita["create"].post(
@@ -56,7 +57,7 @@ const berita = proxy({
);
if (res.status === 200) {
berita.findMany.load();
return toast.success("succes create");
return toast.success("success create");
}
return toast.error("failed create");
@@ -66,28 +67,166 @@ const berita = proxy({
berita.create.loading = false;
}
},
resetForm() {
berita.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.BeritaGetPayload<{
include: {
image: true,
kategoriBerita: true
}
}>[]
| Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.berita["find-many"].get();
if (res.status === 200) {
berita.findMany.data = (res.data?.data as any) ?? [];
berita.findMany.data = (res.data?.data ) ?? [];
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
berita.delete.loading = true;
const response = await fetch(`/api/desa/berita/delete/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Berita berhasil dihapus");
await berita.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus berita");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus berita");
} finally {
berita.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/desa/berita/${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 = {
judul: data.judul,
deskripsi: data.deskripsi,
content: data.content,
kategoriBeritaId: data.kategoriBeritaId || "",
imageId: data.imageId || "",
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(berita.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
berita.edit.loading = true;
const response = await fetch(`/api/desa/berita/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
content: this.form.content,
kategoriBeritaId: this.form.kategoriBeritaId || null,
imageId: this.form.imageId,
}),
});
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 berita");
await berita.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update berita");
}
} catch (error) {
console.error("Error updating berita:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update berita");
return false;
} finally {
berita.edit.loading = false;
}
},
reset() {
berita.edit.id = "";
berita.edit.form = { ...defaultForm };
},
},
});
// 5. State global
const stateDashboardBerita = proxy({
category,
berita,
});
export default stateDashboardBerita;
export default stateDashboardBerita;

View File

@@ -9,7 +9,7 @@ import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { Button, Stack } from '@mantine/core';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
// const content =
// '<h2 style="text-align: center;">Welcome to Mantine rich text editor</h2><p><code>RichTextEditor</code> component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. <code>RichTextEditor</code> is based on <a href="https://tiptap.dev/" rel="noopener noreferrer" target="_blank">Tiptap.dev</a> and supports all of its features:</p><ul><li>General text formatting: <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <s>strike-through</s> </li><li>Headings (h1-h6)</li><li>Sub and super scripts (<sup>&lt;sup /&gt;</sup> and <sub>&lt;sub /&gt;</sub> tags)</li><li>Ordered and bullet lists</li><li>Text align&nbsp;</li><li>And all <a href="https://tiptap.dev/extensions" target="_blank" rel="noopener noreferrer">other extensions</a></li></ul>';
@@ -18,11 +18,18 @@ import { useEffect } from 'react';
onEditorReady,
showSubmit = true,
onSubmit,
initialContent = '',
onUpdate,
}: {
onEditorReady?: (editor: any | null) => void;
onSubmit?: (val: string) => void;
showSubmit?: boolean;
initialContent?: string;
onUpdate?: (content: string) => void;
}) {
const [mounted, setMounted] = useState(false);
const [isReady, setIsReady] = useState(false);
const editor = useEditor({
extensions: [
StarterKit,
@@ -33,14 +40,46 @@ import { useEffect } from 'react';
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
content: '',
content: initialContent || '<p></p>',
onUpdate: ({ editor }) => {
if (onUpdate) {
onUpdate(editor.getHTML());
}
},
editorProps: {
attributes: {
class: 'prose max-w-none',
},
},
onSelectionUpdate: () => {
if (!isReady && editor) {
setIsReady(true);
onEditorReady?.(editor);
}
},
immediatelyRender: false
});
useEffect(() => {
onEditorReady?.(editor);
}, [editor, onEditorReady] );
if (editor) {
// Set initial content when component mounts
editor.commands.setContent(initialContent || '<p></p>');
// Mark as mounted and notify parent
if (!mounted) {
setMounted(true);
onEditorReady?.(editor);
}
}
return () => {
if (editor) {
editor.destroy();
}
};
}, [editor, initialContent, mounted, onEditorReady]);
if (!editor) return null;
if (!editor) return <div>Loading editor...</div>;
return (

View File

@@ -0,0 +1,269 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import {
Box,
Button,
Center,
Image,
Paper,
Select,
Skeleton,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { useProxy } from "valtio/utils";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { FileInput } from "@mantine/core";
import stateDashboardBerita from "../../../../_state/desa/berita";
import { Prisma } from "@prisma/client";
import { useShallowEffect } from "@mantine/hooks";
import { BeritaEditor } from "../../_com/BeritaEditor";
import colors from "@/con/colors";
function BeritaEdit() {
const beritaState = useProxy(stateDashboardBerita);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [editorInstance, setEditorInstance] = useState<any>(null);
const [isEditorReady, setIsEditorReady] = useState(false);
const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || '',
deskripsi: beritaState.berita.edit.form.deskripsi || '',
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '',
content: beritaState.berita.edit.form.content || '',
imageId: beritaState.berita.edit.form.imageId || ''
});
// Load berita by id saat pertama kali
useEffect(() => {
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
kategoriBeritaId: data.kategoriBeritaId || '',
content: data.content || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
}
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
// Handle editor ready
const handleEditorReady = (editor: any) => {
setEditorInstance(editor);
setIsEditorReady(true);
// Set initial content if exists
if (formData.content) {
editor.commands.setContent(formData.content);
}
};
const handleSubmit = async () => {
if (!isEditorReady || !editorInstance) {
return toast.error("Editor belum siap");
}
try {
const htmlContent = editorInstance.getHTML();
if (!htmlContent || htmlContent === "<p></p>") {
return toast.warn("Konten tidak boleh kosong");
}
// Update form data with editor content
const updatedFormData = {
...formData,
content: htmlContent
};
// Update global state with form data
beritaState.berita.edit.form = {
judul: updatedFormData.judul,
deskripsi: updatedFormData.deskripsi,
content: updatedFormData.content,
kategoriBeritaId: updatedFormData.kategoriBeritaId || '',
imageId: beritaState.berita.edit.form.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
beritaState.berita.edit.form.imageId = uploaded.id;
}
await beritaState.berita.edit.update();
toast.success("Berita berhasil diperbarui!");
router.push("/admin/desa/berita");
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
}
};
return (
<Box py={10}>
<IconArrowBack color={colors["blue-button"]} size={30} onClick={() => router.back()}/>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Berita</Title>
<TextInput
value={formData.judul}
onChange={(e) => setFormData({...formData, judul: e.target.value})}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<SelectCategory
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
/>
<TextInput
value={formData.deskripsi}
onChange={(e) => setFormData({...formData, deskripsi: e.target.value})}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<BeritaEditor
initialContent={formData.content}
onEditorReady={handleEditorReady}
showSubmit={false}
onUpdate={(content) => setFormData({...formData, content})}
/>
</Box>
<Button onClick={handleSubmit}>Simpan Perubahan</Button>
</Stack>
</Paper>
</Box>
);
}
interface SelectCategoryProps {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true;
id: true;
};
}> | null) => void;
value?: string | null;
defaultValue?: string | null;
}
function SelectCategory({
onChange,
value,
defaultValue,
}: SelectCategoryProps) {
const categoryState = useProxy(stateDashboardBerita.category);
useShallowEffect(() => {
categoryState.findMany.load().then(() => {
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
});
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
const selectedValue = value || defaultValue;
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
placeholder="Pilih kategori"
data={categoryState.findMany.data.map((item) => ({
label: item.name,
value: item.id,
}))}
value={selectedValue || null}
onChange={(val: string | null) => {
if (val) {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
} else {
onChange(null);
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
);
}
export default BeritaEdit;

View File

@@ -1,16 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { Box, Button, Center, FileInput, Group, Image, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { ActionIcon, Box, Button, Center, FileInput, Flex, Image, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Prisma } from '@prisma/client';
import { IconImageInPicture } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import stateDashboardBerita from '../../_state/desa/berita';
import { BeritaEditor } from './_com/BeritaEditor';
import colors from '@/con/colors';
import { IconEdit, IconImageInPicture, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../_state/desa/berita';
import { BeritaEditor } from './_com/BeritaEditor';
function Page() {
return (
@@ -22,99 +24,6 @@ function Page() {
);
}
// function BeritaCreate() {
// const beritaState = useProxy(stateDashboardBerita);
// const [previewImage, setPreviewImage] = useState<string | null>(null);
// const [file, setFile] = useState<File | null>(null);
// const [editorInstance, setEditorInstance] = useState<any>(null);
// const handleSubmit = async () => {
// if (!file) {
// return toast.warn("Pilih file gambar terlebih dahulu");
// }
// if (!editorInstance) return toast.error("Editor belum siap");
// const htmlContent = editorInstance.getHTML();
// if (!htmlContent || htmlContent === '<p></p>') return toast.warn("Konten tidak boleh kosong");
// beritaState.berita.create.form.content = htmlContent;
// console.log(beritaState.berita.create.form)
// // Upload gambar dulu
// 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");
// }
// // Simpan ID gambar ke form
// beritaState.berita.create.form.imageId = uploaded.id;
// // Submit data berita
// await beritaState.berita.create.create();
// };
// return (
// <Box py={10}>
// <Paper bg={colors["white-1"]} p={"md"}>
// <Stack gap={"xs"}>
// <SelectCategory
// onChange={(val) => {
// beritaState.berita.create.form.kategoriBeritaId = val.id;
// }}
// />
// <TextInput
// onChange={(val) => {
// beritaState.berita.create.form.judul = val.target.value;
// }}
// label={"Judul"}
// placeholder="masukkan judul"
// />
// <TextInput
// onChange={(val) => {
// beritaState.berita.create.form.deskripsi = val.target.value;
// }}
// label={"Deskripsi"}
// placeholder="masukkan deskripsi"
// />
// <FileInput
// label="Upload Gambar"
// onChange={async (e) => {
// if (!e) return;
// setFile(e);
// const base64 = await e.arrayBuffer().then((buf) =>
// "data:image/png;base64," + Buffer.from(buf).toString("base64")
// );
// setPreviewImage(base64);
// }}
// />
// {previewImage ? (
// <Image alt="" src={previewImage} w={200} h={200} />
// ) : (
// <Center w={200} h={200} bg={"gray"}>
// <IconImageInPicture />
// </Center>
// )}
// <BeritaEditor
// showSubmit={false}
// onEditorReady={(ed) => setEditorInstance(ed)}
// />
// <Button onClick={handleSubmit}>Simpan Berita</Button>
// </Stack>
// </Paper>
// </Box>
// );
// }
function BeritaCreate() {
const beritaState = useProxy(stateDashboardBerita);
const [previewImage, setPreviewImage] = useState<string | null>(null);
@@ -175,32 +84,33 @@ function BeritaCreate() {
return (
<Box py={10}>
<Paper bg={colors["white-1"]} p={"md"} w={{base: "100%", md: "50%"}}>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Berita</Title>
<TextInput
value={beritaState.berita.create.form.judul}
onChange={(val) => {
beritaState.berita.create.form.judul = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<SelectCategory
onChange={(val) => {
beritaState.berita.create.form.kategoriBeritaId = val.id;
}}
/>
<TextInput
value={beritaState.berita.create.form.judul}
onChange={(val) => {
beritaState.berita.create.form.judul = val.target.value;
}}
label={"Judul"}
placeholder="masukkan judul"
/>
<TextInput
value={beritaState.berita.create.form.deskripsi}
onChange={(val) => {
beritaState.berita.create.form.deskripsi = val.target.value;
}}
label={"Deskripsi"}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<FileInput
label="Upload Gambar"
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
@@ -218,13 +128,14 @@ function BeritaCreate() {
<IconImageInPicture />
</Center>
)}
<BeritaEditor
showSubmit={false}
onEditorReady={(ed) => setEditorInstance(ed)}
/>
<Button onClick={handleSubmit}>Simpan Berita</Button>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<BeritaEditor
showSubmit={false}
onEditorReady={(ed) => setEditorInstance(ed)}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
</Stack>
</Paper>
</Box>
@@ -234,74 +145,188 @@ function BeritaCreate() {
// function BeritaList() {
// const beritaState = useProxy(stateDashboardBerita)
// useShallowEffect(() => {
// beritaState.berita.findMany.load()
// }, [])
// const router = useRouter()
// if (!beritaState.berita.findMany.data) return <Stack py={10}>
// {Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
// </Stack>
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <Stack>
// <Text fz={"xl"} fw={"bold"}>List Berita</Text>
// <SimpleGrid cols={{ base: 1, md: 4 }}>
// {beritaState.berita.findMany.data?.map((item) => (
// <Paper key={item.id} bg={colors['BG-trans']} p={'md'}>
// <Box >
// <Flex justify="flex-end" mt={10}>
// <ActionIcon
// onClick={() => beritaState.berita.delete.byId(item.id)}
// disabled={beritaState.berita.delete.loading}
// color={colors['blue-button']} variant='transparent'>
// <IconX size={20} />
// </ActionIcon>
// <ActionIcon onClick={() => {
// router.push("/desa/berita/edit");
// }} color={colors['blue-button']} variant='transparent'>
// <IconEdit size={20} />
// </ActionIcon>
// </Flex>
// <Text fw={"bold"} fz={"sm"}>
// Kategori
// </Text>
// <Text>{item.kategoriBerita?.name}</Text>
// <Text fw={"bold"} fz={"sm"}>
// Judul
// </Text>
// <Text>{item.judul}</Text>
// <Text lineClamp={1} fw={"bold"} fz={"sm"}>
// Deskripsi
// </Text>
// <Text size='sm' lineClamp={2}>{item.deskripsi}</Text>
// <Text fw={"bold"} fz={"sm"}>
// Gambar
// </Text>
// <Image w={{ base: 100, md: 150 }} src={item.image?.link} alt="gambar" />
// </Box>
// </Paper>
// ))}
// </SimpleGrid>
// </Stack>
// </Paper>
// </Box>
// )
// }
function BeritaList() {
const beritaState = useProxy(stateDashboardBerita)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
beritaState.berita.findMany.load()
}, [])
if (!beritaState.berita.findMany.data) return <Stack py={10}>
{Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
</Stack>
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
beritaState.berita.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
if (!beritaState.berita.findMany.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>List Berita</Text>
<SimpleGrid cols={{ base: 1, md: 4 }}>
{beritaState.berita.findMany.data?.map((item) => (
<Paper key={item.id} bg={colors['BG-trans']} p={'md'}>
<Box >
<Text fw={"bold"} fz={"sm"}>
Kategori
</Text>
<Text>{item.kategoriBerita?.name}</Text>
<Text fw={"bold"} fz={"sm"}>
Judul
</Text>
<Text>{item.judul}</Text>
<Text lineClamp={1} fw={"bold"} fz={"sm"}>
Deskripsi
</Text>
<Text size='sm' lineClamp={2}>{item.deskripsi}</Text>
<Text fw={"bold"} fz={"sm"}>
Gambar
</Text>
<Image w={200} src={item.image?.link} alt="gambar" />
</Box>
</Paper>
))}
{beritaState.berita.findMany.data?.map((item) => (
<Paper key={item.id} bg={colors['BG-trans']} p={'md'}>
<Box>
<Flex justify="flex-end" mt={10}>
<ActionIcon
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}
disabled={beritaState.berita.delete.loading}
color={colors['blue-button']} variant='transparent'
>
<IconX size={20} />
</ActionIcon>
<ActionIcon
onClick={() => router.push(`/admin/desa/berita/edit/${item.id}`)}
color={colors['blue-button']} variant='transparent'
>
<IconEdit size={20} />
</ActionIcon>
</Flex>
<Text fw={"bold"} fz={"sm"}>Kategori</Text>
<Text>{item.kategoriBerita?.name}</Text>
<Text fw={"bold"} fz={"sm"}>Judul</Text>
<Text>{item.judul}</Text>
<Text lineClamp={1} fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text size='sm' lineClamp={2}>{item.deskripsi}</Text>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={item.image?.link} alt="gambar" />
</Box>
</Paper>
))}
</SimpleGrid>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
)
}
function SelectCategory({ onChange }: {
function SelectCategory({
onChange,
}: {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true,
id: true
}
}>) => void
name: true;
id: true;
};
}>) => void;
}) {
const beritaState = useProxy(stateDashboardBerita)
useShallowEffect(() => {
beritaState.category.findMany.load()
}, [])
const categoryState = useProxy(stateDashboardBerita.category);
if (!beritaState.category.findMany.data) return <Skeleton h={40} />
return <Group>
<Select placeholder='pilih kategori' label={<Text fz={"sm"} fw={"bold"}>Pilih Kategori</Text>} data={beritaState.category.findMany.data.map((item) => ({
value: item.id,
label: item.name
}))} onChange={(v) => {
const data = beritaState.category.findMany.data?.find((item) => item.id === v)
if (!data) return
onChange(data)
}} />
</Group>
useShallowEffect(() => {
categoryState.findMany.load();
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
placeholder="Pilih kategori"
data={categoryState.findMany.data.map((item) => ({
label: item.name,
value: item.id,
}))}
onChange={(val) => {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
}}
searchable
nothingFoundMessage="Tidak ditemukan"
/>
);
}
export default Page;

View File

@@ -1,10 +1,46 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function Page() {
return (
<div>
demografi-pekerjaan
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Demografi Pekerjaan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Pekerja Laki - Laki</Text>}
placeholder="Masukkan jumlah pekerja laki - laki"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Pekerja Perempuan</Text>}
placeholder="Masukkan jumlah pekerja perempuan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pekerjaan</Text>}
placeholder="Masukkan nama pekerjaan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Demografi Pekerjaan</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,10 +1,42 @@
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import colors from '@/con/colors';
import React from 'react';
function Page() {
return (
<div>
jumlah-penduduk-miskin-2024-2025
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper p={"md"} bg={colors['white-1']} w={{ base: '100%', md: '50%' }}>
<Stack gap={"xs"}>
<Title order={3}>Jumlah Penduduk Miskin 2024-2025</Title>
<TextInput
label="Tahun"
placeholder="masukkan tahun"
/>
<TextInput
label="Jumlah Penduduk Miskin"
placeholder="masukkan jumlah penduduk miskin"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']} >
<Stack gap={"xs"}>
<Title order={3}>Grafik Jumlah Penduduk Miskin 2024-2025</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,11 +1,37 @@
import React from 'react';
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import DataPengangguran from './ui/dataPengangguran/page';
import GrafikDataPengangguran from './ui/grafikDataPengangguran/page';
import DetailDataPengangguran from './ui/detailDataPengangguran/page';
function Page() {
return (
<div>
jumlah-pengangguran-2024-2025
</div>
);
<Stack>
<Title order={3}>Jumlah Pengangguran 2024-2025</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Data Pengangguran Desa"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Data Pengangguran Desa">
Data Pengangguran Desa
</TabsTab>
<TabsTab value="Grafik Data Pengangguran Desa">
Grafik Data Pengangguran Desa
</TabsTab>
<TabsTab value="Detail Data Pengangguran Desa">
Detail Data Pengangguran Desa
</TabsTab>
</TabsList>
<TabsPanel value="Data Pengangguran Desa">
<DataPengangguran/>
</TabsPanel>
<TabsPanel value="Grafik Data Pengangguran Desa">
<GrafikDataPengangguran/>
</TabsPanel>
<TabsPanel value="Detail Data Pengangguran Desa">
<DetailDataPengangguran/>
</TabsPanel>
</Tabs>
</Stack>
)
}
export default Page;

View File

@@ -0,0 +1,51 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
function DataPengangguran() {
return (
<Box>
<Box py={15}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Data Pengangguran</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Total Pengangguran</Text>}
placeholder="masukkan total pengangguran"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Pengangguran Terdidik</Text>}
placeholder="masukkan pengangguran terdidik"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Usia Produktif</Text>}
placeholder="masukkan usia produktif"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Sedang Mencari Kerja</Text>}
placeholder="masukkan jumlah sedang mencari kerja"
/>
<Group>
<Button bg={colors['blue-button']}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>List Data Pengangguran</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
</Box>
);
}
export default DataPengangguran;

View File

@@ -0,0 +1,18 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
function DetailDataPengangguran() {
return (
<Box>
<Box py={15}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Detail Data Pengangguran</Title>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default DetailDataPengangguran;

View File

@@ -0,0 +1,49 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
function GrafikDataPengangguran() {
return (
<Box>
<Box py={15}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Data Pengangguran</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Total Pengangguran</Text>}
placeholder="masukkan total pengangguran"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Pengangguran Terdidik</Text>}
placeholder="masukkan pengangguran terdidik"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Usia Produktif</Text>}
placeholder="masukkan usia produktif"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Sedang Mencari Kerja</Text>}
placeholder="masukkan jumlah sedang mencari kerja"
/>
<Group>
<Button bg={colors['blue-button']}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>List Data Pengangguran</Title>
</Stack>
</Paper>
</Box>
</Box>
</Box>
);
}
export default GrafikDataPengangguran;

View File

@@ -1,10 +1,30 @@
import React from 'react';
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import ProgramKemiskinan from './ui/program/page';
import StatistikKemiskinan from './ui/statistik/page';
function Page() {
return (
<div>
program-kemiskinan
</div>
<Stack>
<Title order={3}>Program Kemiskinan</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Program Kemiskinan"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Program Kemiskinan">
Program Kemiskinan
</TabsTab>
<TabsTab value="Statistik Kemiskinan">
Statistik Kemiskinan
</TabsTab>
</TabsList>
<TabsPanel value="Program Kemiskinan">
<ProgramKemiskinan />
</TabsPanel>
<TabsPanel value="Statistik Kemiskinan">
<StatistikKemiskinan />
</TabsPanel>
</Tabs>
</Stack>
);
}

View File

@@ -0,0 +1,43 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function ProgramKemiskinan() {
return (
<Box py={15}>
<SimpleGrid cols={{base: 1, md: 2}}>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Program Kemiskinan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program</Text>}
placeholder="Masukkan nama program"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Program</Text>}
placeholder="Masukkan deskripsi program"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>List Data Program Kemiskinan</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default ProgramKemiskinan;

View File

@@ -0,0 +1,43 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function StatistikKemiskinan() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<Box>
<Paper w={{base: '100%', md: '50%'}} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Statistik Kemiskinan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="Masukkan tahun"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Penduduk Miskin</Text>}
placeholder="Masukkan jumlah penduduk miskin"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Statistik Kemiskinan</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default StatistikKemiskinan;

View File

@@ -1,10 +1,31 @@
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import React from 'react';
import DataSektorUnggulan from './ui/data_sektor_unggulan/page';
import GrafikDataSektor from './ui/grafik_data_sektor/page';
function Page() {
return (
<div>
sektor-unggulan-desa
</div>
<Stack>
<Title order={3}>Sektor Unggulan Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Data Sektor Unggulan Desa"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Data Sektor Unggulan Desa">
Data Sektor Unggulan Desa
</TabsTab>
<TabsTab value="Grafik Data Sektor Unggulan Desa">
Grafik Data Sektor Unggulan Desa
</TabsTab>
</TabsList>
<TabsPanel value="Data Sektor Unggulan Desa">
<DataSektorUnggulan />
</TabsPanel>
<TabsPanel value="Grafik Data Sektor Unggulan Desa">
<GrafikDataSektor />
</TabsPanel>
</Tabs>
</Stack>
);
}

View File

@@ -0,0 +1,45 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function DataSektorUnggulan() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Data Sektor Unggulan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Sektor Unggulan</Text>}
placeholder="Masukkan nama sektor unggulan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Sektor Unggulan</Text>}
placeholder="Masukkan deskripsi sektor unggulan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Data Sektor Unggulan</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default DataSektorUnggulan;

View File

@@ -0,0 +1,44 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function GrafikDataSektor() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<Box>
<Paper w={{base: '100%', md: '50%'}} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Data Sektor Unggulan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Sektor Unggulan</Text>}
placeholder="Masukkan nama sektor unggulan"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Jumlah Sektor Unggulan</Text>}
placeholder="Masukkan jumlah sektor unggulan"
/>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Grafik Sektor Unggulan</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default GrafikDataSektor;

View File

@@ -1,10 +1,43 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import React from 'react';
function Page() {
return (
<div>
ajukan-ide-inovatif
</div>
<Box py={5}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>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>
</Paper>
</Box>
);
}

View File

@@ -1,10 +1,49 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconImageInPicture } from '@tabler/icons-react';
import React from 'react';
function Page() {
return (
<div>
desa-digital-smart-village
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Create Data Desa Digital Smart Village</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Desa Digital Smart Village</Text>}
placeholder="Masukkan nama desa digital smart village"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Desa Digital Smart Village</Text>}
placeholder="Masukkan deskripsi desa digital smart village"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>
Upload Gambar Desa Digital Smart Village
</Text>
<IconImageInPicture size={24} />
</Box>
<Group>
<Button
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={3}>Create Data Desa Digital Smart Village</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,35 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconImageInPicture } from '@tabler/icons-react';
import React from 'react';
function CreateInfoTeknologiTepatGuna() {
return (
<Box>
<Stack gap={'xs'}>
<Paper w={{base: "100%", md: "50%"}} p={"md"} bg={colors['white-1']}>
<Stack gap={"xs"}>
<Title order={4}>Create Info Teknologi Tepat Guna</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={24}/>
</Box>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Info Teknologi Tepat Guna</Text>}
placeholder="Masukkan nama info teknologi tepat guna"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Info Teknologi Tepat Guna</Text>}
placeholder="Masukkan deskripsi info teknologi tepat guna"
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default CreateInfoTeknologiTepatGuna;

View File

@@ -0,0 +1,31 @@
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Stack, Title } from '@mantine/core';
import React from 'react';
function ListDataInfoTeknologiTepatGuna() {
return (
<Box>
<Stack gap={'xs'}>
<Paper p={'md'} bg={colors['BG-trans']}>
<Title order={4}>List Data Info Teknologi Tepat Guna</Title>
<SimpleGrid py={10} cols={{ base: 1, md: 4 }}>
<Paper p={'md'} bg={colors['white-1']}>
Data 1
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Data 2
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Data 3
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Data 4
</Paper>
</SimpleGrid>
</Paper>
</Stack>
</Box>
);
}
export default ListDataInfoTeknologiTepatGuna;

View File

@@ -1,10 +1,17 @@
import { Box, Stack, Title } from '@mantine/core';
import React from 'react';
import CreateInfoTeknologiTepatGuna from './create/page';
import ListDataInfoTeknologiTepatGuna from './listData/page';
function Page() {
return (
<div>
info-teknologi-tepat-guna
</div>
<Box>
<Stack gap={'xs'}>
<Title order={3}>Info Teknologi Tepat Guna</Title>
<CreateInfoTeknologiTepatGuna/>
<ListDataInfoTeknologiTepatGuna/>
</Stack>
</Box>
);
}

View File

@@ -1,10 +1,34 @@
import colors from '@/con/colors';
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import React from 'react';
import KolaborasiInovasi from './ui/kolaborasiInovasi/page';
import MitraKolaborasi from './ui/mitraKolaborasi/page';
function Page() {
return (
<div>
kolaborasi-inovasi
</div>
<Box>
<Stack>
<Title order={3}>Kolaborasi Inovasi</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Kolaborasi Inovasi"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Kolaborasi Inovasi">
Kolaborasi Inovasi
</TabsTab>
<TabsTab value="Mitra Kolaborasi">
Mitra Kolaborasi
</TabsTab>
</TabsList>
<TabsPanel value="Kolaborasi Inovasi">
<KolaborasiInovasi/>
</TabsPanel>
<TabsPanel value="Mitra Kolaborasi">
<MitraKolaborasi/>
</TabsPanel>
</Tabs>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,53 @@
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import React from 'react';
function KolaborasiInovasi() {
return (
<Box py={15}>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kolaborasi Inovasi</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="Masukkan tahun"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
placeholder="Masukkan nama kolaborasi inovasi"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Kolaborasi Inovasi</Text>}
placeholder="Masukkan deskripsi kolaborasi inovasi"
/>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['BG-trans']} p={'md'}>
<Title order={4}>List Data Kolaborasi Inovasi</Title>
<SimpleGrid py={10} cols={{ base: 1, md: 4 }}>
<Paper p={'md'} bg={colors['white-1']}>
Data 1
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Data 2
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Data 3
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Data 4
</Paper>
</SimpleGrid>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default KolaborasiInovasi;

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import colors from '@/con/colors';
import { IconImageInPicture } from '@tabler/icons-react';
function MitraKolaborasi() {
return (
<Box py={15}>
<Stack gap={'xs'}>
<Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack gap={'xs'}>
<Title order={4}>Create Mitra Kolaborasi</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Mitra Kolaborasi</Text>}
placeholder="Masukkan nama mitra kolaborasi"
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50}/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['BG-trans']} p={'md'}>
<Title order={4}>List Data Kolaborasi Inovasi</Title>
<SimpleGrid py={10} cols={{ base: 1, md: 4 }}>
<Paper p={'md'} bg={colors['white-1']}>
Foto 1
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Foto 2
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Foto 3
</Paper>
<Paper p={'md'} bg={colors['white-1']}>
Foto 4
</Paper>
</SimpleGrid>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default MitraKolaborasi;

View File

@@ -1,10 +1,40 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconImageInPicture } from '@tabler/icons-react';
import React from 'react';
function Page() {
return (
<div>
layanan-online-desa
</div>
<Box>
<Stack gap={"xs"}>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={'xs'}>
<Title order={3}>Layanan Online Desa</Title>
<TextInput
label={<Text fz={'sm'} fw={'bold'}>Nama Layanan</Text>}
placeholder="Masukkan nama layanan"
/>
<TextInput
label={<Text fz={'sm'} fw={'bold'}>Deskripsi Layanan</Text>}
placeholder="Masukkan deskripsi layanan"
/>
<Box>
<Text fz={'sm'} fw={'bold'}>Upload Gambar Layanan</Text>
<IconImageInPicture size={24} />
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={'xs'}>
<Title order={3}>List Data Layanan Online Desa</Title>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,37 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconImageInPicture } from '@tabler/icons-react';
import React from 'react';
function ProgramKreatifCreate() {
return (
<Box>
<Stack gap={"xs"}>
<Paper w={{base: "100%", md: "50%"}} p={"md"} bg={colors["white-1"]}>
<Stack gap={"xs"}>
<Title order={4}>Create Program Kreatif Desa</Title>
<Box>
<Text fz={"sm"} fw={"bold"}>Gambar</Text>
<IconImageInPicture size={24}/>
</Box>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program</Text>}
placeholder="Masukkan nama program"
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="Masukkan deskripsi"
/>
<Group>
<Button
bg={colors["blue-button"]}
>Simpan</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default ProgramKreatifCreate;

View File

@@ -0,0 +1,29 @@
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Stack, Title } from '@mantine/core';
import React from 'react';
function ListDataProgramKreatifDesa() {
return (
<Box>
<Stack gap={"xs"}>
<Title order={3}>List Data Program Kreatif Desa</Title>
<SimpleGrid cols={{ base: 1, md: 4 }}>
<Paper p={"md"} bg={colors["white-1"]}>
Data 1
</Paper>
<Paper p={"md"} bg={colors["white-1"]}>
Data 2
</Paper>
<Paper p={"md"} bg={colors["white-1"]}>
Data 3
</Paper>
<Paper p={"md"} bg={colors["white-1"]}>
Data 4
</Paper>
</SimpleGrid>
</Stack>
</Box>
);
}
export default ListDataProgramKreatifDesa;

View File

@@ -1,10 +1,17 @@
import { Box, Stack, Title } from '@mantine/core';
import React from 'react';
import ProgramKreatifCreate from './create/page';
import ListDataProgramKreatifDesa from './listData/page';
function Page() {
return (
<div>
program-kreatif-desa
</div>
<Box>
<Stack gap={"xs"}>
<Title order={3}>Program Kreatif Desa</Title>
<ProgramKreatifCreate />
<ListDataProgramKreatifDesa />
</Stack>
</Box>
);
}

View File

@@ -36,7 +36,6 @@ function GrafikHasilKepuasan() {
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
w={{ base: '100%', md: '50%' }}
label="Label"
placeholder="masukkan label"
value={grafikHasilKepuasan.create.form.label}
@@ -45,7 +44,6 @@ function GrafikHasilKepuasan() {
}}
/>
<TextInput
w={{ base: '100%', md: '50%' }}
label="Jumlah Kepuasan"
type="number"
placeholder="masukkan jumlah kepuasan"

View File

@@ -0,0 +1,55 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
const beritaDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const berita = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true, // pastikan relasi image sudah ada di prisma schema
},
});
if (!berita) {
return {
status: 404,
body: "Berita tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (berita.image) {
try {
const filePath = path.join(berita.image.path, berita.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: berita.image.id },
});
} catch (err) {
console.error("Gagal hapus file image:", err);
}
}
// Hapus berita dari DB
await prisma.berita.delete({
where: { id },
});
return {
success: true,
message: "Berita dan file terkait berhasil dihapus",
};
};
export default beritaDelete;

View File

@@ -0,0 +1,65 @@
import prisma from "@/lib/prisma";
export default async function handler(
request: Request
) {
// Extract the ID from the URL path
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
// Validate that the ID is a valid UUID or whatever format you're using
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Berita tidak ditemukan",
}, { status: 404 });
}
// Ensure we're returning a proper Response object
return new Response(JSON.stringify({
success: true,
message: "Success fetch berita by ID",
data,
}), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (e) {
console.error("Find by ID error:", e);
return new Response(JSON.stringify({
success: false,
message: "Gagal mengambil berita: " + (e instanceof Error ? e.message : 'Unknown error'),
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
}

View File

@@ -2,10 +2,17 @@ import Elysia, { t } from "elysia";
import kategoriBeritaFindMany from "./category";
import beritaFindMany from "./find-many";
import beritaCreate from "./create";
import beritaDelete from "./del";
import beritaUpdate from "./updt";
import findBeritaById from "./find-by-id";
const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
.get("/category/find-many", kategoriBeritaFindMany)
.get("/find-many", beritaFindMany)
.get("/:id", async (context) => {
const response = await findBeritaById(new Request(context.request));
return response;
})
.post("/create", beritaCreate, {
body: t.Object({
judul: t.String(),
@@ -14,6 +21,23 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
content: t.String(),
kategoriBeritaId: t.Union([t.String(), t.Null()]),
}),
});
})
.delete("/delete/:id", beritaDelete)
.put(
"/:id",
async (context) => {
const response = await beritaUpdate(context);
return response;
},
{
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
imageId: t.String(),
content: t.String(),
kategoriBeritaId: t.Union([t.String(), t.Null()]),
}),
}
);
export default Berita;

View File

@@ -0,0 +1,168 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import { Prisma } from "@prisma/client";
import fs from "fs/promises";
import path from "path";
type FormUpdate = Prisma.BeritaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
content: true;
kategoriBeritaId: true;
imageId: true;
};
}>;
// async function beritaUpdate(context: Context) {
// const body = (await context.body) as FormUpdate;
// const {
// id,
// judul,
// deskripsi,
// content,
// kategoriBeritaId,
// imageId,
// } = body;
// if (!id) {
// return {
// status: 400,
// body: "ID tidak boleh kosong",
// };
// }
// const existing = await prisma.berita.findUnique({
// where: { id },
// include: {
// image: true,
// },
// });
// if (!existing) {
// return {
// status: 404,
// body: "Berita tidak ditemukan",
// };
// }
// // Cek jika imageId diubah
// if (existing.imageId && existing.imageId !== imageId) {
// const oldImage = existing.image;
// if (oldImage) {
// try {
// const filePath = path.join(oldImage.path, oldImage.name);
// await fs.unlink(filePath); // hapus file dari filesystem
// await prisma.fileStorage.delete({
// where: { id: oldImage.id }, // hapus record dari DB
// });
// } catch (err) {
// console.error("Gagal hapus gambar lama:", err);
// }
// }
// }
// const updated = await prisma.berita.update({
// where: { id },
// data: {
// judul,
// deskripsi,
// content,
// kategoriBeritaId,
// imageId,
// },
// });
// return {
// success: true,
// message: "Berita berhasil diupdate (gambar lama juga dihapus jika ada)",
// data: updated,
// };
// }
// export default beritaUpdate;
async function beritaUpdate(context: Context) {
try {
const id = context.params?.id as string; // ambil dari URL
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
judul,
deskripsi,
content,
kategoriBeritaId,
imageId,
} = body;
if (!id) {
return new Response(
JSON.stringify({ success: false, message: "ID tidak boleh kosong" }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const existing = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true,
},
});
if (!existing) {
return new Response(
JSON.stringify({ success: false, message: "Berita tidak ditemukan" }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.berita.update({
where: { id },
data: {
judul,
deskripsi,
content,
kategoriBeritaId: kategoriBeritaId || null,
imageId,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Berita berhasil diupdate",
data: updated,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error("Error updating berita:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate berita",
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
export default beritaUpdate;

View File

@@ -0,0 +1,60 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
const fileStorageDelete = async (context: Context) => {
const { params } = context;
const id = params?.id as string;
if (!id) {
return {
status: 400,
body: "ID file tidak ditemukan",
};
}
if (!UPLOAD_DIR) {
return {
status: 500,
body: "UPLOAD_DIR belum dikonfigurasi",
};
}
// Cek file dari database
const file = await prisma.fileStorage.findUnique({
where: { id },
});
if (!file) {
return {
status: 404,
body: "File tidak ditemukan di database",
};
}
const filePath = path.join(file.path, file.name);
try {
// Hapus file dari filesystem
await fs.unlink(filePath);
} catch (err) {
console.error("Gagal hapus file:", err);
// Tetap lanjutkan hapus dari database meskipun file fisik tidak ditemukan
}
// Hapus dari database
await prisma.fileStorage.delete({
where: { id },
});
return {
message: "File berhasil dihapus",
deletedId: id,
};
};
export default fileStorageDelete;

View File

@@ -2,6 +2,7 @@ import Elysia, { t } from "elysia";
import fileStorageCreate from "./_lib/create";
import fileStorageFindUnique from "./_lib/findUniq";
import { fileStorageFindMany } from "./_lib/findMany";
import fileStorageDelete from "./_lib/del";
const FileStorage = new Elysia({
prefix: "/api/fileStorage",
@@ -14,6 +15,7 @@ const FileStorage = new Elysia({
}),
})
.get("/findUnique/:name", fileStorageFindUnique)
.get("/findMany", fileStorageFindMany);
.get("/findMany", fileStorageFindMany)
.delete("/delete/:id", fileStorageDelete);
export default FileStorage;