UI & API Menu Inovasi, SubMenu Program Kreatif Desa

This commit is contained in:
2025-07-14 17:14:02 +08:00
parent 6e109ffe00
commit ae328f40a0
26 changed files with 1068 additions and 169 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"scripts": {
"dev": "next dev --turbopack",

View File

@@ -1341,3 +1341,15 @@ model DesaDigital {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PROGRAM KREATIF ========================================= //
model ProgramKreatif {
id String @id @default(cuid())
name String
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -0,0 +1,227 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
slug: "",
icon: "",
};
const programKreatifState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKreatifState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form
);
if (res.status === 200) {
programKreatifState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
programKreatifState.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
programKreatifState.findMany.loading = true; // Use the full path to access the property
programKreatifState.findMany.page = page;
try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || [];
programKreatifState.findMany.total = res.data.total || 0;
programKreatifState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
} finally {
programKreatifState.findMany.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/inovasi/programkreatif/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
slug: data.slug,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/inovasi/programkreatif/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await programKreatifState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data program kreatif");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ProgramKreatifGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/programkreatif/${id}`);
if (res.ok) {
const data = await res.json();
programKreatifState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
programKreatifState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading program kreatif:", error);
programKreatifState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKreatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/programkreatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program kreatif berhasil dihapus");
await programKreatifState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kreatif");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kreatif");
} finally {
programKreatifState.delete.loading = false;
}
},
},
});
export default programKreatifState;

View File

@@ -18,8 +18,6 @@ import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";

View File

@@ -1,7 +1,7 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/ekonomi/desa-digital';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';

View File

@@ -7,7 +7,7 @@ import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import desaDigitalState from '../../../_state/ekonomi/desa-digital';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function DetailDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState)

View File

@@ -8,7 +8,7 @@ import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/ekonomi/desa-digital';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function CreateDesaDigital() {

View File

@@ -8,7 +8,7 @@ import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import desaDigitalState from '../../_state/ekonomi/desa-digital';
import desaDigitalState from '../../_state/inovasi/desa-digital';
function DesaDigitalSmartVillage() {
const [search, setSearch] = useState("");

View File

@@ -3,7 +3,7 @@ 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";
// import { KeamananEditor } from "../../../keamanan/_com/keamananEditor";
export default function EditLayananOnlineDesa() {
const router = useRouter();
@@ -28,9 +28,9 @@ export default function EditLayananOnlineDesa() {
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Layanan Online Desa</Text>
<KeamananEditor
{/* <KeamananEditor
showSubmit={false}
/>
/> */}
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>

View File

@@ -0,0 +1,149 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { 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';
import SelectIconProgramEdit from '../../_lib/selectIconEdit';
interface FormProgramKreatif {
name: string;
deskripsi: string;
slug: string;
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah';
function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateProgramKreatif.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = {
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramKreatif.update.form = {
...stateProgramKreatif.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
icon: formData.icon.trim(),
}
await stateProgramKreatif.update.submit();
router.push("/admin/inovasi/program-kreatif-desa");
} catch (error) {
console.error("Error updating program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Edit Program Kreatif Desa</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<TextInput
value={formData.slug}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="masukkan deskripsi singkat program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
slug: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateProgramKreatif.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramKreatif.update.form.icon = value;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,127 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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, IconChartLine, IconEdit, IconLeaf, IconRecycle, IconTent, IconTrophy, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramKreatif = useProxy(programKreatifState)
const router = useRouter()
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
};
useShallowEffect(() => {
stateProgramKreatif.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateProgramKreatif.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/program-kreatif-desa")
}
}
if (!stateProgramKreatif.findUnique.data) {
return (
<Stack>
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{iconMap[stateProgramKreatif.findUnique.data?.icon] && (
<Box title={stateProgramKreatif.findUnique.data?.icon}>
{React.createElement(iconMap[stateProgramKreatif.findUnique.data?.icon], { size: 24 })}
</Box>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.slug}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramKreatif.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
setSelectedId(stateProgramKreatif.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateProgramKreatif.delete.loading || !stateProgramKreatif.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
router.push(`/admin/inovasi/program-kreatif-desa/${stateProgramKreatif.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramKreatif.findUnique.data}
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 program kreatif desa ini?"
/>
</Box>
);
}
export default DetailProgramKreatifDesa;

View File

@@ -0,0 +1,75 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
import { useEffect, useState } from 'react';
const iconMap = {
ekowisata: { label: 'Ekowisata', icon: IconLeaf },
kompetisi: { label: 'Kompetisi', icon: IconTrophy },
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
label: data.label,
}));
export default function SelectIconProgram(
{ onChange }: { onChange: (value: IconKey) => void }) {
const [selectedIcon, setSelectedIcon] = useState<IconKey>('ekowisata');
const IconComponent = iconMap[selectedIcon]?.icon || null;
// Push default icon ke state saat render awal
useEffect(() => {
onChange(selectedIcon);
}, []);
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={selectedIcon}
onChange={(value) => {
if (value) {
setSelectedIcon(value as IconKey);
onChange(value as IconKey);
}
}}
data={iconList}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
</Box>
)
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
},
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,70 @@
'use client'
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
const iconMap = {
ekowisata: { label: 'Ekowisata', icon: IconLeaf },
kompetisi: { label: 'Kompetisi', icon: IconTrophy },
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
label: data.label,
}));
export default function SelectIconProgramEdit({
onChange,
value,
}: {
onChange: (value: IconKey) => void;
value: IconKey;
}) {
const IconComponent = iconMap[value]?.icon || null;
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={value}
onChange={(value) => {
if (value) onChange(value as IconKey);
}}
data={iconList}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
</Box>
)
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
},
}}
/>
</Box>
);
}

View File

@@ -1,48 +1,70 @@
'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 { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../_lib/selectIcon';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
slug: "",
deskripsi: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Program Kreatif Desa</Title>
<Title order={3}>Create Program Kreatif Desa</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kreatif Desa</Text>}
placeholder='Masukkan nama program kreatif desa'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</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 DetailProgramKreatifDesa() {
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 Program Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>Test Deskripsi Singkat</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/program-kreatif-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 DetailProgramKreatifDesa;

View File

@@ -1,49 +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 EditProgramKreatifDesa() {
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 Program Kreatif Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kreatif Desa</Text>}
placeholder='Masukkan nama Program Kreatif Desa'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -1,58 +1,155 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import programKreatifState from '../../_state/inovasi/program-kreatif';
import { useProxy } from 'valtio/utils';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
function ProgramKreatifDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Kreatif Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramKreatifDesa/>
<ListProgramKreatifDesa search={search} />
</Box>
);
}
function ListProgramKreatifDesa() {
function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program kreatif desa yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Program Kreatif Desa 1</TableTd>
<TableTd>Image</TableTd>
<TableTd>Deskripsi Singkat</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/program-kreatif-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.slug }}></TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)}>
<IconDeviceImac size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default ProgramKreatifDesa;

View File

@@ -1,10 +1,12 @@
import Elysia from "elysia";
import DesaDigital from "./desa-digital";
import ProgramKreatif from "./program-kreatif";
const Inovasi = new Elysia({
prefix: "/api/inovasi",
tags: ["Inovasi"],
})
.use(DesaDigital)
.use(ProgramKreatif)
export default Inovasi;

View File

@@ -0,0 +1,30 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreateProgramKreatif = {
name: string;
slug: string;
deskripsi: string;
icon: string;
}
export default async function programKreatifCreate(context: Context){
const body = context.body as FormCreateProgramKreatif;
await prisma.programKreatif.create({
data: {
name: body.name,
slug: body.slug,
deskripsi: body.deskripsi,
icon: body.icon,
}
})
return {
success: true,
message: "Success create program kreatif",
data: {
...body,
}
}
}

View File

@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function programKreatifDelete(context: Context) {
const { id } = context.params as { id: string };
if (!id) {
return {
success: false,
message: "ID program kreatif tidak ditemukan",
};
}
try {
const deleted = await prisma.programKreatif.delete({
where: { id },
});
return {
success: true,
message: "Program kreatif berhasil dihapus",
data: deleted,
};
} catch (error: any) {
console.error("Error delete program kreatif:", error);
return {
success: false,
message: "Terjadi kesalahan saat menghapus program kreatif",
error: error.message,
};
}
}

View File

@@ -0,0 +1,44 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
// Di findMany.ts
export default async function programKreatifFindMany(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.programKreatif.findMany({
where: { isActive: true },
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.programKreatif.count({
where: { isActive: true }
})
]);
const totalPages = Math.ceil(total / limit);
return {
success: true,
message: "Success fetch program kreatif with pagination",
data,
page,
totalPages,
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch program kreatif with pagination",
data: [],
page: 1,
totalPages: 1,
total: 0,
};
}
}

View File

@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function programKreatifFindUnique(context: Context) {
const { id } = context.params as { id: string };
if (!id) {
return {
success: false,
message: "ID program kreatif diperlukan",
};
}
try {
const programKreatif = await prisma.programKreatif.findUnique({
where: { id },
});
if (!programKreatif) {
return {
success: false,
message: "Program kreatif tidak ditemukan",
};
}
return {
success: true,
data: programKreatif,
};
} catch (error: any) {
console.error("Error findUnique program kreatif:", error);
return {
success: false,
message: "Gagal mengambil data program kreatif",
error: error.message,
};
}
}

View File

@@ -0,0 +1,41 @@
import Elysia, { t } from "elysia";
import programKreatifFindMany from "./findMany";
import programKreatifFindUnique from "./findUnique";
import programKreatifCreate from "./create";
import programKreatifUpdate from "./updt";
import programKreatifDelete from "./del";
const ProgramKreatif = new Elysia({
prefix: "/programkreatif",
tags: ["Inovasi/Program Kreatif"],
})
.get("/find-many", programKreatifFindMany)
.get("/:id", async (context) => {
const response = await programKreatifFindUnique(context);
return response;
})
.post("/create", programKreatifCreate, {
body: t.Object({
name: t.String(),
slug: t.String(),
deskripsi: t.String(),
icon: t.String(),
}),
})
.put(
"/:id",
async (context) => {
const response = await programKreatifUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
slug: t.String(),
deskripsi: t.String(),
icon: t.String(),
}),
}
)
.delete("/del/:id", programKreatifDelete);
export default ProgramKreatif;

View File

@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdateProgramKreatif = {
id: string;
name?: string;
slug?: string;
deskripsi?: string;
icon?: string;
};
export default async function programKreatifUpdate(context: Context) {
const body = context.body as FormUpdateProgramKreatif;
const id = context.params?.id; // ambil dari URL param
if (!id) {
return {
success: false,
message: "ID program kreatif wajib diisi",
};
}
try {
const updated = await prisma.programKreatif.update({
where: { id },
data: {
name: body.name,
slug: body.slug,
deskripsi: body.deskripsi,
icon: body.icon,
},
});
return {
success: true,
message: "Program kreatif berhasil diupdate",
data: updated,
};
} catch (error: any) {
console.error("Error update program kreatif:", error);
return {
success: false,
message: "Gagal mengupdate program kreatif",
error: error.message,
};
}
}