UI & API Pendidikan Non Formal

This commit is contained in:
2025-07-28 17:51:42 +08:00
parent ac0eb926eb
commit e2e1672c80
28 changed files with 1241 additions and 75 deletions

View File

@@ -0,0 +1,267 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PENDIDIKAN NON FORMAL ========================================= //
const templateTujuanPendidikanNonFormalForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanPendidikanNonFormalForm =
Prisma.TujuanPendidikanNonFormalGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanPendidikanNonFormal = proxy({
findById: {
data: null as TujuanPendidikanNonFormalForm | null,
loading: false,
initialize() {
stateTujuanPendidikanNonFormal.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanPendidikanNonFormalForm;
},
async load(id: string) {
try {
stateTujuanPendidikanNonFormal.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanPendidikanNonFormal.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan pendidikan non formal");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data tujuan pendidikan non formal"
);
} finally {
stateTujuanPendidikanNonFormal.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanPendidikanNonFormalForm) {
const cek = templateTujuanPendidikanNonFormalForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTujuanPendidikanNonFormal.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan pendidikan non formal berhasil diubah");
await stateTujuanPendidikanNonFormal.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan pendidikan non formal");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data tujuan pendidikan non formal"
);
} finally {
stateTujuanPendidikanNonFormal.update.loading = false;
}
},
},
});
// ========================================= TEMPAT KEGIATAN ========================================= //
const templateTempatKegiatanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TempatKegiatanForm = Prisma.TempatKegiatanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTempatKegiatan = proxy({
findById: {
data: null as TempatKegiatanForm | null,
loading: false,
initialize() {
stateTempatKegiatan.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TempatKegiatanForm;
},
async load(id: string) {
try {
stateTempatKegiatan.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTempatKegiatan.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tempat kegiatan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tempat kegiatan");
} finally {
stateTempatKegiatan.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TempatKegiatanForm) {
const cek = templateTempatKegiatanForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTempatKegiatan.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tempat kegiatan berhasil diubah");
await stateTempatKegiatan.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tempat kegiatan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tempat kegiatan");
} finally {
stateTempatKegiatan.update.loading = false;
}
},
},
});
// ========================================= JENIS PROGRAM YANG DISELENGGARAKAN ========================================= //
const templateJenisProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type JenisProgramForm = Prisma.JenisProgramYangDiselenggarakanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateJenisProgram = proxy({
findById: {
data: null as JenisProgramForm | null,
loading: false,
initialize() {
stateJenisProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as JenisProgramForm;
},
async load(id: string) {
try {
stateJenisProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateJenisProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data jenis program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data jenis program");
} finally {
stateJenisProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: JenisProgramForm) {
const cek = templateJenisProgramForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateJenisProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data jenis program berhasil diubah");
await stateJenisProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data jenis program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data jenis program");
} finally {
stateJenisProgram.update.loading = false;
}
},
},
});
const pendidikanNonFormalState = proxy({
stateTujuanPendidikanNonFormal,
stateTempatKegiatan,
stateJenisProgram,
});
export default pendidikanNonFormalState;

View File

@@ -0,0 +1,68 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Tujuan Program",
value: "tujuan-program",
href: "/admin/pendidikan/pendidikan-non-formal/tujuan-program"
},
{
label: "Tempat Kegiatan",
value: "tempat-kegiatan",
href: "/admin/pendidikan/pendidikan-non-formal/tempat-kegiatan"
},
{
label: "Jenis Program yang Diselenggarakan",
value: "jenis-program-yang-diselenggarakan",
href: "/admin/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Pendidikan Non Formal</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;

View File

@@ -0,0 +1,99 @@
'use client'
import { Button, Stack } from '@mantine/core';
import { Link, RichTextEditor } from '@mantine/tiptap';
import Highlight from '@tiptap/extension-highlight';
import SubScript from '@tiptap/extension-subscript';
import Superscript from '@tiptap/extension-superscript';
import TextAlign from '@tiptap/extension-text-align';
import Underline from '@tiptap/extension-underline';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect } from 'react';
export function PendidikanNonFormalTextEditor({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
onSubmit?: (val: string) => void,
onChange: (val: string) => void,
showSubmit?: boolean,
initialContent?: string }) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
immediatelyRender: false,
content: initialContent,
onUpdate : ({editor}) => {
onChange(editor.getHTML())
}
});
useEffect(() => {
if (editor && initialContent !== editor.getHTML()) {
editor.commands.setContent(initialContent || '<p></p>');
}
}, [initialContent, editor]);
return (
<Stack >
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
{showSubmit && (
<Button onClick={() => {
if (!editor) return
onSubmit?.(editor?.getHTML())
}}>Submit</Button>
)}
</Stack>
);
}

View File

@@ -0,0 +1,86 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const JenisProgramYangDiselenggarakanTextEditor = dynamic(() => import('../../_lib/pendidikanNonFormalTextEditor').then(mod => mod.PendidikanNonFormalTextEditor), {
ssr: false,
});
function EditJenisProgramYangDiselenggarakan() {
const router = useRouter()
const editState = useProxy(pendidikanNonFormalState.stateJenisProgram)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!editState.findById.data) {
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (editState.findById.data) {
setJudul(editState.findById.data.judul ?? '')
setContent(editState.findById.data.deskripsi ?? '')
}
}, [editState.findById.data])
const submit = () => {
if (editState.findById.data) {
editState.findById.data.judul = judul;
editState.findById.data.deskripsi = content;
editState.update.save(editState.findById.data)
}
router.push('/admin/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Jenis Program Yang Diselenggarakan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
value={judul}
onChange={(e) => setJudul(e.target.value)}
/>
<Text fw={"bold"}>Deskripsi</Text>
<JenisProgramYangDiselenggarakanTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={editState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditJenisProgramYangDiselenggarakan;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pendidikanNonFormalState from '../../../_state/pendidikan/pendidikan-non-formal';
function Page() {
const router = useRouter()
const listPreview = useProxy(pendidikanNonFormalState.stateJenisProgram)
useShallowEffect(() => {
listPreview.findById.load('edit')
}, [])
if (!listPreview.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Jenis Program Yang Diselenggarakan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

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

View File

@@ -1,74 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
function PendidikanNonFormal() {
return (
<Box>
<HeaderSearch
title='Pendidikan Non Formal'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPendidikanNonFormal/>
</Box>
);
}
function ListPendidikanNonFormal() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Pendidikan Non Formal'
href='/admin/pendidikan/pendidikan-non-formal/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Nomor</TableTh>
<TableTh>Nama Lengkap</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>1</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Email</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/pendidikan-non-formal/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default PendidikanNonFormal;

View File

@@ -0,0 +1,86 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const TempatKegiatanTextEditor = dynamic(() => import('../../_lib/pendidikanNonFormalTextEditor').then(mod => mod.PendidikanNonFormalTextEditor), {
ssr: false,
});
function EditTempatKegiatan() {
const router = useRouter()
const editState = useProxy(pendidikanNonFormalState.stateTempatKegiatan)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!editState.findById.data) {
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (editState.findById.data) {
setJudul(editState.findById.data.judul ?? '')
setContent(editState.findById.data.deskripsi ?? '')
}
}, [editState.findById.data])
const submit = () => {
if (editState.findById.data) {
editState.findById.data.judul = judul;
editState.findById.data.deskripsi = content;
editState.update.save(editState.findById.data)
}
router.push('/admin/pendidikan/pendidikan-non-formal/tempat-kegiatan')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Tempat Kegiatan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
value={judul}
onChange={(e) => setJudul(e.target.value)}
/>
<Text fw={"bold"}>Deskripsi</Text>
<TempatKegiatanTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={editState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditTempatKegiatan;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import statePendidikanNonFormal from '../../../_state/pendidikan/pendidikan-non-formal';
function Page() {
const router = useRouter()
const listPreview = useProxy(statePendidikanNonFormal.stateTempatKegiatan)
useShallowEffect(() => {
listPreview.findById.load('edit')
}, [])
if (!listPreview.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Tempat Kegiatan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -0,0 +1,86 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const PendidikanNonFormalTextEditor = dynamic(() => import('../../_lib/pendidikanNonFormalTextEditor').then(mod => mod.PendidikanNonFormalTextEditor), {
ssr: false,
});
function EditTujuanProgram() {
const router = useRouter()
const editState = useProxy(pendidikanNonFormalState.stateTujuanPendidikanNonFormal)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!editState.findById.data) {
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (editState.findById.data) {
setJudul(editState.findById.data.judul ?? '')
setContent(editState.findById.data.deskripsi ?? '')
}
}, [editState.findById.data])
const submit = () => {
if (editState.findById.data) {
editState.findById.data.judul = judul;
editState.findById.data.deskripsi = content;
editState.update.save(editState.findById.data)
}
router.push('/admin/pendidikan/pendidikan-non-formal/tujuan-program')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Tujuan Program</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
value={judul}
onChange={(e) => setJudul(e.target.value)}
/>
<Text fw={"bold"}>Deskripsi</Text>
<PendidikanNonFormalTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={editState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditTujuanProgram;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import statePendidikanNonFormal from '../../../_state/pendidikan/pendidikan-non-formal';
function Page() {
const router = useRouter()
const listPreview = useProxy(statePendidikanNonFormal.stateTujuanPendidikanNonFormal)
useShallowEffect(() => {
listPreview.findById.load('edit')
}, [])
if (!listPreview.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Pendidikan Non Formal</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/pendidikan-non-formal/tujuan-program/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -364,7 +364,7 @@ export const navBar = [
{
id: "Pendidikan_5",
name: "Pendidikan Non Formal",
path: "/admin/pendidikan/pendidikan-non-formal"
path: "/admin/pendidikan/pendidikan-non-formal/tujuan-program"
},
{
id: "Pendidikan_6",

View File

@@ -2,6 +2,7 @@ import Elysia from "elysia";
import InfoSekolahPAUD from "./info-sekolah-paud";
import ProgramPendidikanAnak from "./program-pendidikan-anak";
import BimbinganBelajarDesa from "./bimbingan-belajar-desa";
import PendidikanNonFormal from "./pendidikan-non-formal";
const Pendidikan = new Elysia({
prefix: "/api/pendidikan",
@@ -11,5 +12,6 @@ const Pendidikan = new Elysia({
.use(InfoSekolahPAUD)
.use(ProgramPendidikanAnak)
.use(BimbinganBelajarDesa)
.use(PendidikanNonFormal)
export default Pendidikan;

View File

@@ -0,0 +1,14 @@
import Elysia from "elysia";
import JenisProgramYangDiselenggarakan from "./jenis-program-yang-diselenggarakan";
import TujuanPendidikanNonFormal from "./tujuan-program";
import TempatKegiatan from "./tempat-kegiatan";
const PendidikanNonFormal = new Elysia({
prefix: "/pendidikannonformal",
tags: ["Pendidikan/Pendidikan Non Formal"]
})
.use(JenisProgramYangDiselenggarakan)
.use(TujuanPendidikanNonFormal)
.use(TempatKegiatan)
export default PendidikanNonFormal

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function jenisProgramYangDiselenggarakanFindUnique(
context: Context
) {
try {
const id = context?.params?.slugs?.[0];
// If no ID provided, get the first profile
if (!id) {
const data = await prisma.jenisProgramYangDiselenggarakan.findFirst();
return {
success: true,
data,
};
}
const data = await prisma.jenisProgramYangDiselenggarakan.findUniqueOrThrow(
{
where: { id },
}
);
return {
success: true,
data,
};
} catch (error) {
console.error("Error fetching jenis program yang diselenggarakan:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error",
};
}
}

View File

@@ -0,0 +1,19 @@
import Elysia, { t } from "elysia";
import jenisProgramYangDiselenggarakanFindUnique from "./findUnique";
import jenisProgramYangDiselenggarakanUpdate from "./updt";
const JenisProgramYangDiselenggarakan = new Elysia({
prefix: "/jenisprogramyangdiselenggarakan",
tags: ["Pendidikan/Pendidikan Non Formal/Jenis Program Yang Diselenggarakan"]
})
.get("/find-by-id", jenisProgramYangDiselenggarakanFindUnique)
.post("/update", jenisProgramYangDiselenggarakanUpdate, {
body: t.Object({
id: t.String(),
judul: t.String(),
deskripsi: t.String(),
})
})
export default JenisProgramYangDiselenggarakan

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = Prisma.JenisProgramYangDiselenggarakanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
}
}>
export default async function jenisProgramYangDiselenggarakanUpdate(context: Context) {
const body = context.body as FormUpdate;
await prisma.jenisProgramYangDiselenggarakan.update({
where: {
id: body.id
},
data: {
judul: body.judul,
deskripsi: body.deskripsi,
}
})
return {
success: true,
message: "Jenis program yang diselenggarakan berhasil diupdate",
}
}

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function tempatKegiatanFindUnique(
context: Context
) {
try {
const id = context?.params?.slugs?.[0];
// If no ID provided, get the first profile
if (!id) {
const data = await prisma.tempatKegiatan.findFirst();
return {
success: true,
data,
};
}
const data = await prisma.tempatKegiatan.findUniqueOrThrow(
{
where: { id },
}
);
return {
success: true,
data,
};
} catch (error) {
console.error("Error fetching tempat kegiatan:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error",
};
}
}

View File

@@ -0,0 +1,18 @@
import Elysia, { t } from "elysia";
import tempatKegiatanFindUnique from "./findUnique";
import tempatKegiatanUpdate from "./updt";
const TempatKegiatan = new Elysia({
prefix: "/tempatkegiatan",
tags: ["Pendidikan/Pendidikan Non Formal/Tempat Kegiatan"]
})
.get("/find-by-id", tempatKegiatanFindUnique)
.post("/update", tempatKegiatanUpdate, {
body: t.Object({
id: t.String(),
judul: t.String(),
deskripsi: t.String(),
})
})
export default TempatKegiatan

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = Prisma.TempatKegiatanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
}
}>
export default async function tempatKegiatanUpdate(context: Context) {
const body = context.body as FormUpdate;
await prisma.tempatKegiatan.update({
where: {
id: body.id
},
data: {
judul: body.judul,
deskripsi: body.deskripsi,
}
})
return {
success: true,
message: "Tempat kegiatan berhasil diupdate",
}
}

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function tujuanPendidikanNonFormalFindUnique(
context: Context
) {
try {
const id = context?.params?.slugs?.[0];
// If no ID provided, get the first profile
if (!id) {
const data = await prisma.tujuanPendidikanNonFormal.findFirst();
return {
success: true,
data,
};
}
const data = await prisma.tujuanPendidikanNonFormal.findUniqueOrThrow(
{
where: { id },
}
);
return {
success: true,
data,
};
} catch (error) {
console.error("Error fetching tujuan pendidikan non formal:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error",
};
}
}

View File

@@ -0,0 +1,18 @@
import Elysia, { t } from "elysia";
import tujuanPendidikanNonFormalFindUnique from "./findUnique";
import tujuanPendidikanNonFormalUpdate from "./updt";
const TujuanPendidikanNonFormal = new Elysia({
prefix: "/tujuanpendidikannonformal",
tags: ["Pendidikan/Pendidikan Non Formal/Tujuan Pendidikan Non Formal"]
})
.get("/find-by-id", tujuanPendidikanNonFormalFindUnique)
.post("/update", tujuanPendidikanNonFormalUpdate, {
body: t.Object({
id: t.String(),
judul: t.String(),
deskripsi: t.String(),
})
})
export default TujuanPendidikanNonFormal

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.TujuanPendidikanNonFormalGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
}
}>
export default async function tujuanPendidikanNonFormalUpdate(context: Context) {
const body = context.body as FormCreate;
await prisma.tujuanPendidikanNonFormal.update({
where: {
id: body.id
},
data: {
judul: body.judul,
deskripsi: body.deskripsi,
}
})
return {
success: true,
message: "Tujuan pendidikan non formal berhasil diupdate",
}
}