Compare commits
10 Commits
nico/14-ok
...
nico/23-ok
| Author | SHA1 | Date | |
|---|---|---|---|
| f82c7b86e0 | |||
| b5d6585cd5 | |||
| aa98359ef7 | |||
| 0ff0d5234a | |||
| 827c1c191a | |||
| fb596f9033 | |||
| 9055b40769 | |||
| bbf13c1cf7 | |||
| 75bf0652b1 | |||
| 0b574406e2 |
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --bun next dev --hostname 0.0.0.0",
|
||||
"dev": "bun --bun next dev",
|
||||
"build": "bun --bun next build",
|
||||
"start": "bun --bun next start"
|
||||
},
|
||||
@@ -43,6 +43,7 @@
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"add": "^2.0.6",
|
||||
"adm-zip": "^0.5.16",
|
||||
"animate.css": "^4.1.1",
|
||||
@@ -52,6 +53,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"colors": "^1.4.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.3",
|
||||
"elysia": "^1.3.5",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
@@ -71,6 +73,7 @@
|
||||
"next": "^15.5.2",
|
||||
"next-view-transitions": "^0.3.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^7.0.10",
|
||||
"p-limit": "^6.2.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.6",
|
||||
@@ -82,6 +85,7 @@
|
||||
"react-simple-toasts": "^6.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"readdirp": "^4.1.1",
|
||||
"recharts": "^2.15.3",
|
||||
"sharp": "^0.34.3",
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
[
|
||||
{
|
||||
"id": "cmds8w2q60002vnbe6i8qhkuo",
|
||||
"name": "Telephone Desa Darmasaba",
|
||||
"iconUrl": "081239580000",
|
||||
"imageId": "cmff3nv180003vn6h5jvedidq"
|
||||
},
|
||||
{
|
||||
"id": "cmds8z7u20005vnbegyyvnbk0",
|
||||
"name": "Email Desa Darmasaba",
|
||||
"iconUrl": "desadarmasaba@badungkab.go.id",
|
||||
"imageId": "cmff3ll130001vn6hkhls3f5y"
|
||||
},
|
||||
{
|
||||
"id": "cmds9023u0008vnbe3oxmhwyf",
|
||||
"name": "Desa Darmasaba",
|
||||
|
||||
@@ -1606,7 +1606,7 @@ model Pembiayaan {
|
||||
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
|
||||
}
|
||||
|
||||
// ========================================= INOVASI ========================================= //
|
||||
// ========================================= MENU INOVASI ========================================= //
|
||||
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
||||
model DesaDigital {
|
||||
id String @id @default(cuid())
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 378 KiB |
@@ -75,17 +75,18 @@ const berita = proxy({
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
const startTime = Date.now();
|
||||
berita.findMany.loading = true;
|
||||
berita.findMany.page = page;
|
||||
berita.findMany.search = search;
|
||||
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
|
||||
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
|
||||
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
berita.findMany.data = res.data.data ?? [];
|
||||
berita.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
@@ -98,9 +99,16 @@ const berita = proxy({
|
||||
berita.findMany.data = [];
|
||||
berita.findMany.totalPages = 1;
|
||||
} finally {
|
||||
berita.findMany.loading = false;
|
||||
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
|
||||
const elapsed = Date.now() - startTime;
|
||||
const minDelay = 300;
|
||||
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
|
||||
|
||||
setTimeout(() => {
|
||||
berita.findMany.loading = false;
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
|
||||
@@ -55,81 +55,95 @@ const dataPerpustakaan = proxy({
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.page = page;
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
const startTime = Date.now();
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.page = page;
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||
"findMany"
|
||||
].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
|
||||
const elapsed = Date.now() - startTime;
|
||||
const minDelay = 300;
|
||||
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
|
||||
|
||||
setTimeout(() => {
|
||||
dataPerpustakaan.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
findManyAll: {
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = {};
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findManyAll"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||
} else {
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
},
|
||||
findManyAll: {
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = {};
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||
"findManyAll"
|
||||
].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||
} else {
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
} finally {
|
||||
dataPerpustakaan.findManyAll.loading = false;
|
||||
}
|
||||
},
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
} finally {
|
||||
dataPerpustakaan.findManyAll.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
@@ -356,17 +370,20 @@ const kategoriBuku = proxy({
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
kategoriBuku.findMany.page = page;
|
||||
kategoriBuku.findMany.search = search;
|
||||
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
|
||||
|
||||
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
|
||||
"findMany"
|
||||
].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kategoriBuku.findMany.data = res.data.data ?? [];
|
||||
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
@@ -557,7 +574,7 @@ const templatePeminjamanBuku = z.object({
|
||||
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
|
||||
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
|
||||
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
|
||||
catatan: z.string().min(1, "Catatan harus diisi")
|
||||
catatan: z.string().min(1, "Catatan harus diisi"),
|
||||
});
|
||||
|
||||
const defaultPeminjamanBuku = {
|
||||
@@ -568,7 +585,7 @@ const defaultPeminjamanBuku = {
|
||||
tanggalPinjam: "",
|
||||
batasKembali: "",
|
||||
tanggalKembali: "",
|
||||
catatan: ""
|
||||
catatan: "",
|
||||
};
|
||||
|
||||
interface FormEditData {
|
||||
@@ -584,7 +601,7 @@ interface FormEditData {
|
||||
batasKembali: string;
|
||||
tanggalKembali: string;
|
||||
catatan: string;
|
||||
status: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan';
|
||||
status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
|
||||
}
|
||||
|
||||
const editForm: FormEditData = {
|
||||
@@ -596,8 +613,8 @@ const editForm: FormEditData = {
|
||||
batasKembali: "",
|
||||
tanggalKembali: "",
|
||||
catatan: "",
|
||||
status: "Dipinjam"
|
||||
}
|
||||
status: "Dipinjam",
|
||||
};
|
||||
|
||||
const peminjamanBuku = proxy({
|
||||
create: {
|
||||
@@ -646,13 +663,16 @@ const peminjamanBuku = proxy({
|
||||
peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
peminjamanBuku.findMany.page = page;
|
||||
peminjamanBuku.findMany.search = search;
|
||||
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku["findMany"].get({ query });
|
||||
|
||||
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
|
||||
"findMany"
|
||||
].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
peminjamanBuku.findMany.data = res.data.data ?? [];
|
||||
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
@@ -720,7 +740,9 @@ const peminjamanBuku = proxy({
|
||||
);
|
||||
await peminjamanBuku.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus Data Peminjaman Buku");
|
||||
toast.error(
|
||||
result?.message || "Gagal menghapus Data Peminjaman Buku"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
@@ -768,7 +790,7 @@ const peminjamanBuku = proxy({
|
||||
batasKembali: data.batasKembali,
|
||||
tanggalKembali: data.tanggalKembali,
|
||||
catatan: data.catatan,
|
||||
status: data.status
|
||||
status: data.status,
|
||||
};
|
||||
return data; // Return the loaded data
|
||||
} else {
|
||||
@@ -811,7 +833,7 @@ const peminjamanBuku = proxy({
|
||||
batasKembali: this.form.batasKembali,
|
||||
tanggalKembali: this.form.tanggalKembali,
|
||||
catatan: this.form.catatan,
|
||||
status: this.form.status
|
||||
status: this.form.status,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -830,7 +852,9 @@ const peminjamanBuku = proxy({
|
||||
await peminjamanBuku.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update data peminjaman buku");
|
||||
throw new Error(
|
||||
result.message || "Gagal update data peminjaman buku"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating data peminjaman buku:", error);
|
||||
@@ -849,7 +873,7 @@ const peminjamanBuku = proxy({
|
||||
peminjamanBuku.update.form = { ...editForm };
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const perpustakaanDigitalState = proxy({
|
||||
dataPerpustakaan,
|
||||
|
||||
@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
stateProgram.findUnique
|
||||
.load(id)
|
||||
.then(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
await stateProgram.findUnique.load(id);
|
||||
const data = stateProgram.findUnique.data;
|
||||
if (data) {
|
||||
setFormData({
|
||||
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
} catch (err) {
|
||||
console.error('Error load data:', err);
|
||||
toast.error('Gagal mengambil data program');
|
||||
});
|
||||
}, [id, stateProgram.findUnique]);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]); // ✅ hanya trigger saat id berubah
|
||||
|
||||
|
||||
// generic handler untuk field top-level
|
||||
const handleChange = useCallback(
|
||||
|
||||
@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Profil Desa
|
||||
Profile Desa
|
||||
</Title>
|
||||
|
||||
<Tabs
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
router.push("/login");
|
||||
router.push("/darmasaba");
|
||||
}}
|
||||
color={colors["blue-button"]}
|
||||
radius="xl"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
import { proxy, subscribe } from 'valtio';
|
||||
import { proxy } from 'valtio';
|
||||
import { debounce } from 'lodash';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
interface SearchResult {
|
||||
type?: string; // optional biar gak error
|
||||
type?: string;
|
||||
id: string | number;
|
||||
title?: string;
|
||||
[key: string]: any;
|
||||
@@ -21,9 +21,13 @@ const searchState = proxy({
|
||||
loading: false,
|
||||
|
||||
async fetch() {
|
||||
if (!searchState.query) return;
|
||||
if (!searchState.query) {
|
||||
searchState.results = [];
|
||||
return;
|
||||
}
|
||||
|
||||
searchState.loading = true;
|
||||
|
||||
|
||||
try {
|
||||
const res = await ApiFetch.api.search.findMany.get({
|
||||
query: {
|
||||
@@ -33,21 +37,25 @@ const searchState = proxy({
|
||||
type: searchState.type,
|
||||
},
|
||||
});
|
||||
|
||||
const data = (res.data?.data || []).map((item: any) => ({
|
||||
type: item.type ?? 'unknown', // pastikan selalu ada type
|
||||
...item,
|
||||
}));
|
||||
|
||||
|
||||
console.log("Search API Response:", res);
|
||||
const rawItems = res.data?.data || [];
|
||||
const parsedItems = structuredClone(rawItems); // ✅ penting!
|
||||
|
||||
console.log("✅ Parsed items:", parsedItems);
|
||||
|
||||
if (searchState.page === 1) {
|
||||
searchState.results = data;
|
||||
searchState.results = parsedItems;
|
||||
} else {
|
||||
searchState.results.push(...data);
|
||||
searchState.results.push(...parsedItems);
|
||||
}
|
||||
|
||||
console.log("Search results render:", searchState.results);
|
||||
|
||||
|
||||
searchState.nextPage = res.data?.nextPage || null;
|
||||
} catch (e) {
|
||||
console.error('Search fetch error:', e);
|
||||
} catch (error) {
|
||||
console.error("Search fetch error:", error);
|
||||
} finally {
|
||||
searchState.loading = false;
|
||||
}
|
||||
@@ -60,15 +68,95 @@ const searchState = proxy({
|
||||
},
|
||||
});
|
||||
|
||||
// 🔁 Auto debounce search trigger
|
||||
const debouncedFetch = debounce(() => {
|
||||
if (!searchState.query) return;
|
||||
// 🕒 debounce-nya tetap kita export biar bisa dipanggil manual
|
||||
export const debouncedFetch = debounce(() => {
|
||||
searchState.page = 1;
|
||||
searchState.fetch();
|
||||
}, 500);
|
||||
|
||||
subscribe(searchState, () => {
|
||||
debouncedFetch();
|
||||
});
|
||||
|
||||
export default searchState;
|
||||
|
||||
|
||||
// 'use client';
|
||||
// import { proxy, subscribe } from 'valtio';
|
||||
// import { debounce } from 'lodash';
|
||||
// import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
// interface SearchResult {
|
||||
// type?: string;
|
||||
// id: string | number;
|
||||
// title?: string;
|
||||
// [key: string]: any;
|
||||
// }
|
||||
|
||||
// const searchState = proxy({
|
||||
// query: '',
|
||||
// page: 1,
|
||||
// limit: 10,
|
||||
// type: '', // kosong = global search
|
||||
// results: [] as SearchResult[],
|
||||
// nextPage: null as number | null,
|
||||
// loading: false,
|
||||
|
||||
// // --- fetch utama ---
|
||||
// async fetch() {
|
||||
// if (!searchState.query.trim()) {
|
||||
// // 🧹 kalau query kosong, kosongin data dan stop
|
||||
// searchState.results = [];
|
||||
// searchState.nextPage = null;
|
||||
// searchState.loading = false;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// searchState.loading = true;
|
||||
|
||||
// try {
|
||||
// const res = await ApiFetch.api.search.findMany.get({
|
||||
// query: {
|
||||
// query: searchState.query,
|
||||
// page: searchState.page,
|
||||
// limit: searchState.limit,
|
||||
// type: searchState.type,
|
||||
// },
|
||||
// });
|
||||
|
||||
// const newData = res.data?.data || [];
|
||||
|
||||
// // Kalau ini page pertama, replace data
|
||||
// if (searchState.page === 1) {
|
||||
// searchState.results = newData;
|
||||
// } else {
|
||||
// // Kalau page berikutnya, append data
|
||||
// searchState.results = [...searchState.results, ...newData];
|
||||
// }
|
||||
|
||||
// searchState.nextPage = res.data?.nextPage || null;
|
||||
// } catch (err) {
|
||||
// console.error('Search fetch error:', err);
|
||||
// } finally {
|
||||
// searchState.loading = false;
|
||||
// }
|
||||
// },
|
||||
|
||||
// // --- load next page (infinite scroll) ---
|
||||
// async next() {
|
||||
// if (!searchState.nextPage || searchState.loading) return;
|
||||
// searchState.page = searchState.nextPage;
|
||||
// await searchState.fetch();
|
||||
// },
|
||||
// });
|
||||
|
||||
// // --- debounce agar gak fetch tiap ketik ---
|
||||
// const debouncedFetch = debounce(() => {
|
||||
// // reset pagination setiap query berubah
|
||||
// searchState.page = 1;
|
||||
// searchState.fetch();
|
||||
// }, 500);
|
||||
|
||||
// // --- auto trigger setiap query berubah ---
|
||||
// subscribe(searchState, () => {
|
||||
// // kalau query berubah, jalankan debounce fetch
|
||||
// debouncedFetch();
|
||||
// });
|
||||
|
||||
// export default searchState;
|
||||
|
||||
54
src/app/api/subscribe/route.ts
Normal file
54
src/app/api/subscribe/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { email } = await request.json();
|
||||
|
||||
// Input validation
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Email is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Email regex validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Invalid email format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Configure nodemailer
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
// Send email
|
||||
await transporter.sendMail({
|
||||
from: `"Tim Info" <${process.env.EMAIL_USER}>`,
|
||||
to: email,
|
||||
subject: '✅ Berhasil Berlangganan!',
|
||||
html: `<p>Terima kasih telah berlangganan info terbaru dari kami!</p>`,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Subscription successful! Please check your email.',
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in subscribe API:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,11 +62,23 @@ function Page() {
|
||||
Informasi dan Pelayanan Administrasi Digital
|
||||
</Text>
|
||||
</Box>
|
||||
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy"/>
|
||||
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.content || '' }} />
|
||||
<Text
|
||||
py={20}
|
||||
fz={{ base: "sm", md: "lg" }}
|
||||
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
|
||||
ta="justify"
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: state.findUnique.data?.content || "",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -6,122 +6,83 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
interface FileItem {
|
||||
id: string;
|
||||
name: string;
|
||||
link: string;
|
||||
realName: string;
|
||||
createdAt: string | Date;
|
||||
category: string;
|
||||
path: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export default function FotoContent() {
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
// Handle search and pagination changes
|
||||
const loadData = useCallback((pageNum: number, searchTerm: string) => {
|
||||
id: string;
|
||||
name: string;
|
||||
link: string;
|
||||
realName: string;
|
||||
createdAt: string | Date;
|
||||
category: string;
|
||||
path: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export default function FotoContent() {
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const limit = 9; // ✅ ambil 12 data per page
|
||||
|
||||
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
|
||||
setLoading(true);
|
||||
// Using the load function from the component's scope
|
||||
const loadFn = async () => {
|
||||
try {
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get({
|
||||
query: {
|
||||
category: 'image',
|
||||
page: pageNum.toString(),
|
||||
limit: '10',
|
||||
...(searchTerm && { search: searchTerm })
|
||||
}
|
||||
});
|
||||
try {
|
||||
const query: Record<string, string> = {
|
||||
category: 'image',
|
||||
page: pageNum.toString(),
|
||||
limit: limit.toString(),
|
||||
};
|
||||
if (searchTerm) query.search = searchTerm;
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
setFiles(response.data.data || []);
|
||||
setTotalPages(response.data.meta?.totalPages || 1);
|
||||
} else {
|
||||
setFiles([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Load error:', err);
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
setFiles(response.data.data || []);
|
||||
setTotalPages(response.data.meta?.totalPages || 1);
|
||||
} else {
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadFn();
|
||||
} catch (err) {
|
||||
console.error('Load error:', err);
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial load and URL change handler
|
||||
// ✅ Initial load + update when URL/search changes
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlSearch = urlParams.get('search') || '';
|
||||
const urlPage = parseInt(urlParams.get('page') || '1');
|
||||
|
||||
setSearch(urlSearch);
|
||||
setPage(urlPage);
|
||||
loadData(urlPage, urlSearch);
|
||||
};
|
||||
|
||||
// Handle search updates from the search bar
|
||||
const handleSearchUpdate = (e: Event) => {
|
||||
const { search } = (e as CustomEvent).detail;
|
||||
|
||||
setSearch(search);
|
||||
setPage(1); // Reset to first page on new search
|
||||
setPage(1);
|
||||
loadData(1, search);
|
||||
};
|
||||
|
||||
// Initial load
|
||||
handleRouteChange();
|
||||
|
||||
// Set up event listeners
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
||||
|
||||
// Cleanup
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handleRouteChange);
|
||||
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// ✅ Fetch data
|
||||
// ✅ Update when page/search changes
|
||||
useEffect(() => {
|
||||
const fetchFiles = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const query: Record<string, string> = {
|
||||
category: 'image',
|
||||
page: page.toString(),
|
||||
limit: '10',
|
||||
};
|
||||
if (search) query.search = search;
|
||||
loadData(page, search);
|
||||
}, [page, search, loadData]);
|
||||
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
setFiles(response.data.data || []);
|
||||
setTotalPages(response.data.meta?.totalPages || 1);
|
||||
} else {
|
||||
setFiles([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch error:', err);
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (page > 0) fetchFiles(); // jangan fetch jika page belum valid
|
||||
}, [search, page]);
|
||||
|
||||
// ✅ Update URL
|
||||
const updateURL = (newSearch: string, newPage: number) => {
|
||||
const url = new URL(window.location.href);
|
||||
if (newSearch) url.searchParams.set('search', newSearch);
|
||||
@@ -148,7 +109,14 @@ interface FileItem {
|
||||
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
{files.map((file) => (
|
||||
<Paper key={file.id} mb={50} p="md" radius={26} bg={colors['white-trans-1']} style={{ height: '100%' }}>
|
||||
<Paper
|
||||
key={file.id}
|
||||
mb={50}
|
||||
p="md"
|
||||
radius={26}
|
||||
bg={colors['white-trans-1']}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
|
||||
<Image
|
||||
src={file.link}
|
||||
@@ -159,20 +127,18 @@ interface FileItem {
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Stack gap="sm" py={10}>
|
||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
||||
{file.realName || file.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(file.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack gap="sm" py={10}>
|
||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
||||
{file.realName || file.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(file.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
@@ -181,4 +147,4 @@ interface FileItem {
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,24 +146,24 @@ function Page() {
|
||||
<Title order={3}>Ajukan Permohonan</Title>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={<Text fz="sm" fw="bold">NIK</Text>}
|
||||
placeholder="masukkan NIK"
|
||||
placeholder="Masukkan NIK"
|
||||
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Alamat</Text>}
|
||||
placeholder="masukkan alamat"
|
||||
placeholder="Masukkan alamat"
|
||||
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
|
||||
placeholder="masukkan Nomor KK"
|
||||
placeholder="Masukkan Nomor KK"
|
||||
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
|
||||
/>
|
||||
<Select
|
||||
@@ -186,12 +186,11 @@ function Page() {
|
||||
stateCreate.create.form.kategoriId = '';
|
||||
}
|
||||
}}
|
||||
searchable
|
||||
// searchable
|
||||
clearable
|
||||
nothingFoundMessage="Tidak ditemukan"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
||||
Simpan
|
||||
</Button>
|
||||
|
||||
@@ -7,34 +7,52 @@ import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function PelayananPerizinanBerusaha() {
|
||||
const state = useProxy(stateLayananDesa)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [active, setActive] = useState(1);
|
||||
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
const state = useProxy(stateLayananDesa);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [active, setActive] = useState(0);
|
||||
|
||||
const totalSteps = 6;
|
||||
|
||||
const nextStep = () => {
|
||||
if (active < totalSteps - 1) {
|
||||
setActive(active + 1);
|
||||
} else if (active === totalSteps - 1) {
|
||||
setActive(totalSteps); // Mark as completed
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (active > 0) {
|
||||
setActive(active - 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await state.pelayananPerizinanBerusaha.findById.load('edit')
|
||||
await state.pelayananPerizinanBerusaha.findById.load('edit');
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
}, [])
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const data = state.pelayananPerizinanBerusaha.findById.data;
|
||||
|
||||
|
||||
if (!data && !loading) {
|
||||
return (
|
||||
<Center mih={300}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Text fz="lg" fw={500} c="dimmed">Belum ada informasi layanan yang tersedia</Text>
|
||||
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">Kunjungi OSS</Button>
|
||||
<Text fz="lg" fw={500} c="dimmed">
|
||||
Belum ada informasi layanan yang tersedia
|
||||
</Text>
|
||||
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
|
||||
Kunjungi OSS
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
@@ -47,72 +65,111 @@ function PelayananPerizinanBerusaha() {
|
||||
<Loader size="lg" color="blue" />
|
||||
</Center>
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
|
||||
Perizinan Berusaha Berbasis Risiko melalui OSS
|
||||
</Title>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
|
||||
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
|
||||
</Text>
|
||||
</Box>
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
|
||||
Perizinan Berusaha Berbasis Risiko melalui OSS
|
||||
</Title>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
|
||||
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text fz={{ base: 'sm', md: 'md' }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>Alur pendaftaran NIB:</Text>
|
||||
<Stepper active={active} onStepClick={setActive} orientation="vertical" color="blue" radius="md"
|
||||
styles={{
|
||||
step: { padding: '14px 0' },
|
||||
stepBody: { marginLeft: 8 }
|
||||
}}
|
||||
>
|
||||
<StepperStep label="Langkah 1" description="Daftar Akun">
|
||||
<Text fz="sm">Membuat akun di portal OSS</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
|
||||
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 3" description="Pilih KBLI">
|
||||
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 4" description="Unggah Dokumen">
|
||||
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
|
||||
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 6" description="Terbit NIB">
|
||||
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
|
||||
</StepperStep>
|
||||
<StepperCompleted>
|
||||
<Center>
|
||||
<Stack align="center" gap="xs">
|
||||
<IconCheck size={40} color="green" />
|
||||
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</StepperCompleted>
|
||||
</Stepper>
|
||||
<Box>
|
||||
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
|
||||
Alur pendaftaran NIB:
|
||||
</Text>
|
||||
<Stepper
|
||||
active={active}
|
||||
onStepClick={(step) => {
|
||||
if (step <= active) { // Only allow clicking on previous or current steps
|
||||
setActive(step);
|
||||
}
|
||||
}}
|
||||
orientation="vertical"
|
||||
color="blue"
|
||||
radius="md"
|
||||
styles={{
|
||||
step: { padding: '14px 0' },
|
||||
stepBody: { marginLeft: 8 }
|
||||
}}
|
||||
>
|
||||
<StepperStep label="Langkah 1" description="Daftar Akun">
|
||||
<Text fz="sm">Membuat akun di portal OSS</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
|
||||
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 3" description="Pilih KBLI">
|
||||
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 4" description="Unggah Dokumen">
|
||||
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
|
||||
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 6" description="Terbit NIB">
|
||||
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
|
||||
</StepperStep>
|
||||
<StepperCompleted>
|
||||
<Center>
|
||||
<Stack align="center" gap="xs">
|
||||
<IconCheck size={40} color="green" />
|
||||
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</StepperCompleted>
|
||||
</Stepper>
|
||||
|
||||
{active < totalSteps && (
|
||||
<Group justify="center" mt="lg">
|
||||
<Button variant="light" leftSection={<IconArrowLeft size={18} />} onClick={prevStep} disabled={active === 0}>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconArrowLeft size={18} />}
|
||||
onClick={prevStep}
|
||||
disabled={active === 0}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<Button rightSection={<IconArrowRight size={18} />} onClick={nextStep}>
|
||||
Lanjut
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Text fz="sm" ta="justify" c="dimmed" mt="md">
|
||||
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "}
|
||||
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">oss.go.id</a> atau hubungi instansi pemerintah terkait.
|
||||
</Text>
|
||||
</Stack>
|
||||
{active < totalSteps ? (
|
||||
<Button
|
||||
rightSection={active < totalSteps - 1 ? <IconArrowRight size={18} /> : null}
|
||||
onClick={nextStep}
|
||||
>
|
||||
{active === totalSteps - 1 ? 'Selesai' : 'Lanjut'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => setActive(0)}
|
||||
>
|
||||
Mulai Lagi
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text fz="sm" ta="justify" c="dimmed" mt="md">
|
||||
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
|
||||
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
|
||||
oss.go.id
|
||||
</a>{' '}
|
||||
atau hubungi instansi pemerintah terkait.
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PelayananPerizinanBerusaha;
|
||||
export default PelayananPerizinanBerusaha;
|
||||
@@ -10,31 +10,36 @@ import ProfilPerbekel from './ui/profilPerbekel';
|
||||
// import LembagaDesa from './ui/lembagaDesa';
|
||||
import MotoDesa from './ui/motoDesa';
|
||||
import SemuaPerbekel from './ui/semuaPerbekel';
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "50%" }}>
|
||||
<Stack align='center' gap={0}>
|
||||
<Text fz={{base: "h1", md: "2.5rem"}} c={colors["blue-button"]} fw={"bold"}>
|
||||
Profile Desa
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<ProfileDesa />
|
||||
<SejarahDesa />
|
||||
<VisimisiDesa />
|
||||
<LambangDesa />
|
||||
<MaskotDesa />
|
||||
<ProfilPerbekel />
|
||||
<MotoDesa />
|
||||
<SemuaPerbekel/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "50%" }}>
|
||||
<Stack align='center' gap={0}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Profile Desa
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<ProfileDesa />
|
||||
<SejarahDesa />
|
||||
<VisimisiDesa />
|
||||
<LambangDesa />
|
||||
<MaskotDesa />
|
||||
<ProfilPerbekel />
|
||||
<MotoDesa />
|
||||
<SemuaPerbekel />
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,167 +1,3 @@
|
||||
// 'use client'
|
||||
// import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||
// import colors from '@/con/colors';
|
||||
// import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
// import { useProxy } from 'valtio/utils';
|
||||
// import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
// import { useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
|
||||
// function Page() {
|
||||
// const state = useProxy(PendapatanAsliDesa.ApbDesa);
|
||||
|
||||
// useShallowEffect(() => {
|
||||
// state.findMany.load();
|
||||
// }, []);
|
||||
|
||||
// useShallowEffect(() => {
|
||||
// PendapatanAsliDesa.pembiayaan.findMany.load();
|
||||
// PendapatanAsliDesa.belanja.findMany.load();
|
||||
// PendapatanAsliDesa.pendapatan.findMany.load();
|
||||
// }, []);
|
||||
|
||||
// // Get the latest APB data
|
||||
// const latestApb = state.findMany.data?.[0];
|
||||
|
||||
// // Calculate totals
|
||||
// const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
|
||||
// const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
|
||||
// const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
|
||||
|
||||
|
||||
// return (
|
||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
|
||||
// <Box px={{ base: 'md', md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
// <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
|
||||
// Pendapatan Asli Desa
|
||||
// </Text>
|
||||
// <Box px={{ base: "md", md: 100 }}>
|
||||
// <Stack gap="lg" justify="center">
|
||||
// <Paper bg={colors['white-1']} p="xl">
|
||||
// <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||
// {/* Pendapatan Card */}
|
||||
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
// <Stack gap={"xs"}>
|
||||
// <Title order={3}>Pendapatan</Title>
|
||||
// {PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
|
||||
// <Box key={item.id}>
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="md" fw={500}>{item.name}</Text>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
// style: 'currency',
|
||||
// currency: 'IDR',
|
||||
// minimumFractionDigits: 0
|
||||
// }).format(item.value)}</Text>
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
// ))}
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="xl" fw={700} c={colors['blue-button']}>
|
||||
// {new Intl.NumberFormat('id-ID', {
|
||||
// style: 'currency',
|
||||
// currency: 'IDR',
|
||||
// minimumFractionDigits: 0
|
||||
// }).format(totalPendapatan)}
|
||||
// </Text>
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Stack>
|
||||
// </Box>
|
||||
|
||||
// {/* Belanja Card */}
|
||||
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
// <Stack gap={"xs"}>
|
||||
// <Title order={3}>Belanja</Title>
|
||||
// {PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
|
||||
// <Box key={item.id}>
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="md" fw={500}>{item.name}</Text>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
// style: 'currency',
|
||||
// currency: 'IDR',
|
||||
// minimumFractionDigits: 0
|
||||
// }).format(item.value)}</Text>
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
// ))}
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="xl" fw={700} c="orange">
|
||||
// {new Intl.NumberFormat('id-ID', {
|
||||
// style: 'currency',
|
||||
// currency: 'IDR',
|
||||
// minimumFractionDigits: 0
|
||||
// }).format(totalBelanja)}
|
||||
// </Text>
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Stack>
|
||||
// </Box>
|
||||
|
||||
// {/* Pembiayaan Card */}
|
||||
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
// <Stack gap={"xs"}>
|
||||
// <Title order={3}>Pembiayaan</Title>
|
||||
// {PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
|
||||
// <Box key={item.id}>
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="md" fw={500}>{item.name}</Text>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
// style: 'currency',
|
||||
// currency: 'IDR',
|
||||
// minimumFractionDigits: 0
|
||||
// }).format(item.value)}</Text>
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
// ))}
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 6 }}>
|
||||
// <Text fz="xl" fw={700} c="green">
|
||||
// {new Intl.NumberFormat('id-ID', {
|
||||
// style: 'currency',
|
||||
// currency: 'IDR',
|
||||
// minimumFractionDigits: 0
|
||||
// }).format(totalPembiayaan)}
|
||||
// </Text>
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Stack>
|
||||
// </Box>
|
||||
|
||||
// </SimpleGrid>
|
||||
// </Paper>
|
||||
// </Stack>
|
||||
// </Box>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default Page;
|
||||
|
||||
'use client'
|
||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||
import colors from '@/con/colors';
|
||||
@@ -206,32 +42,41 @@ function Page() {
|
||||
<Stack gap="lg" justify="center">
|
||||
<Paper bg={colors['white-1']} p="xl">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||
{/* Pendapatan Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
{/* Pendapatan Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Pendapatan</Title>
|
||||
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
|
||||
{latestApb?.pendapatan?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}</Text>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text
|
||||
fz="md"
|
||||
fw={500}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="xl" fw={700} c={colors['blue-button']}>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}} fz="xl" fw={700} c={colors['blue-button']}>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
@@ -247,18 +92,28 @@ function Page() {
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Belanja</Title>
|
||||
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
|
||||
{latestApb?.belanja?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}</Text>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text
|
||||
fz="md"
|
||||
fw={500}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
@@ -284,18 +139,28 @@ function Page() {
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Pembiayaan</Title>
|
||||
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
|
||||
{latestApb?.pembiayaan?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}</Text>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text
|
||||
fz="md"
|
||||
fw={500}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
@@ -366,5 +231,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
export default Page;
|
||||
@@ -48,7 +48,7 @@ function Page() {
|
||||
p={10}
|
||||
mb={50}
|
||||
h={400}
|
||||
w={150}
|
||||
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
|
||||
data={data.map((item) => ({
|
||||
id: item.id,
|
||||
Pekerjaan: item.pekerjaan,
|
||||
|
||||
@@ -72,21 +72,21 @@ function Page() {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Jumlah Penduduk Usia Kerja Yang Menganggur
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Box px={{ base: "md", md: 50, lg: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Paper p={'lg'}>
|
||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
|
||||
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
|
||||
<PieChart
|
||||
w="100%"
|
||||
h={250} // lebih kecil biar aman di mobile
|
||||
@@ -133,7 +133,7 @@ function Page() {
|
||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||
<PieChart
|
||||
w="100%"
|
||||
h={250} // lebih kecil biar aman di mobile
|
||||
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
|
||||
withLabelsLine
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
|
||||
@@ -199,7 +199,7 @@ function Page() {
|
||||
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.percentageChange}</TableTd>
|
||||
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
|
||||
155
src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx
Normal file
155
src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core';
|
||||
import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
|
||||
|
||||
function DetailProdukPasarUser() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const statePasar = useProxy(pasarDesaState);
|
||||
|
||||
useShallowEffect(() => {
|
||||
statePasar.pasarDesa.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const data = statePasar.pasarDesa.findUnique.data;
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={400} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={20}>
|
||||
{/* Tombol kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali ke daftar produk
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
mx="auto"
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
bg={colors['white-1']}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
{/* Gambar Produk */}
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.nama}
|
||||
radius="md"
|
||||
h={300}
|
||||
w="100%"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
h={300}
|
||||
bg="gray.1"
|
||||
display="flex"
|
||||
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
|
||||
>
|
||||
<Text c="dimmed">Tidak ada gambar</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Detail Produk */}
|
||||
<Stack gap="xs">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
{data.nama || 'Produk Tanpa Nama'}
|
||||
</Text>
|
||||
<Group>
|
||||
<Badge color="green" size="lg" radius="md">
|
||||
Rp {data.harga?.toLocaleString('id-ID')}
|
||||
</Badge>
|
||||
{data.rating && (
|
||||
<Group gap={4}>
|
||||
<IconStar size={18} color="#FFD43B" />
|
||||
<Text fz="md" fw={500}>{data.rating}</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Divider my="sm" />
|
||||
|
||||
{/* Info Tambahan */}
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw={600}>Kategori</Text>
|
||||
<Group gap="xs" mt={4}>
|
||||
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
|
||||
data.KategoriToPasar.map((kategori) => (
|
||||
<Badge key={kategori.id} color="blue" variant="light">
|
||||
{kategori.kategori.nama}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{data.alamatUsaha && (
|
||||
<Group gap={6}>
|
||||
<IconMapPin size={18} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.alamatUsaha}</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{data.kontak && (
|
||||
<Group gap={6}>
|
||||
<IconPhone size={18} color={colors['blue-button']} />
|
||||
<Text fz="md">{data.kontak}</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider my="sm" />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
|
||||
<Text fz="md" c="dimmed" mt={4}>
|
||||
Tidak ada deskripsi.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Tombol Aksi User */}
|
||||
{data.kontak && (
|
||||
<Button
|
||||
mt="md"
|
||||
color="green"
|
||||
size="lg"
|
||||
radius="md"
|
||||
component="a"
|
||||
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
|
||||
target="_blank"
|
||||
>
|
||||
Hubungi Penjual via WhatsApp
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailProdukPasarUser;
|
||||
@@ -105,7 +105,7 @@ function Page() {
|
||||
return (
|
||||
<Stack key={k}>
|
||||
<motion.div
|
||||
onClick={() => router.push(`https://wa.me/${v.kontak?.replace(/\D/g, '')}`)}
|
||||
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.8 }}
|
||||
>
|
||||
@@ -117,7 +117,7 @@ function Page() {
|
||||
h={200}
|
||||
w='100%'
|
||||
style={{ objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
|
||||
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text>
|
||||
|
||||
@@ -28,6 +28,32 @@ function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
// Add this check before the return statement
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||
Sektor Unggulan Desa Darmasaba
|
||||
</Text>
|
||||
<Text c="dimmed" mt="md">
|
||||
Data sektor unggulan belum tersedia
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const chartData = data
|
||||
.filter(item => item?.name && typeof item.value === 'number')
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
sektor: item.name,
|
||||
Ton: item.value,
|
||||
}));
|
||||
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -45,27 +71,34 @@ function Page() {
|
||||
return (
|
||||
<Paper p={'xl'} key={k}>
|
||||
<Text fw={'bold'} fz={'h4'}>{v.name}</Text>
|
||||
<Text fz={'h4'} ta={'justify'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
|
||||
<Text fz={'h4'} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
<Paper p={'xl'}>
|
||||
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text>
|
||||
<BarChart
|
||||
p={10}
|
||||
h={300}
|
||||
data={data.map((item) => ({
|
||||
id: item.id,
|
||||
sektor: item.name,
|
||||
Ton: item.value,
|
||||
}))}
|
||||
dataKey="sektor"
|
||||
series={[
|
||||
{ name: 'Ton', color: colors['blue-button'] },
|
||||
]}
|
||||
tickLine="y"
|
||||
/>
|
||||
</Paper>
|
||||
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
||||
<Paper p="xl">
|
||||
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
|
||||
<Box style={{ width: '100%', minWidth: '600px' }}>
|
||||
<BarChart
|
||||
p={10}
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="sektor"
|
||||
series={[
|
||||
{ name: 'Ton', color: colors['blue-button'] },
|
||||
]}
|
||||
tickLine="y"
|
||||
tooltipAnimationDuration={200}
|
||||
withTooltip
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
xAxisLabel="Sektor"
|
||||
yAxisLabel="Ton"
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
|
||||
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
|
||||
|
||||
// const data = [
|
||||
// {
|
||||
@@ -75,17 +75,23 @@ function Page() {
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Group justify="space-between" mb="md" align='center'>
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Program Kreatif Desa
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder="Cari program kreatif..."
|
||||
leftSection={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
<Grid align='center'>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Program Kreatif Desa
|
||||
</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
radius={"lg"}
|
||||
placeholder='Cari Program Kreatif'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
|
||||
@@ -50,10 +50,12 @@ function Page() {
|
||||
</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
placeholder='Cari kontak darurat, nama, atau nomor...'
|
||||
leftSection={<IconSearch size={20} />}
|
||||
radius={"lg"}
|
||||
placeholder='Cari Kontak Darurat'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "100%", md: "25%" }}
|
||||
/>
|
||||
</Group>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
@@ -95,10 +97,12 @@ function Page() {
|
||||
</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
placeholder='Cari kontak darurat, nama, atau nomor...'
|
||||
leftSection={<IconSearch size={20} />}
|
||||
radius={"lg"}
|
||||
placeholder='Cari Kontak Darurat'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
/>
|
||||
</Group>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { DateTimePicker } from '@mantine/dates';
|
||||
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowRight, IconPlus } from '@tabler/icons-react';
|
||||
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
|
||||
function Page() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -53,14 +53,17 @@ function Page() {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Group justify="space-between" align="center">
|
||||
<BackButton />
|
||||
<TextInput
|
||||
placeholder="Cari laporan"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Flex>
|
||||
radius={"lg"}
|
||||
placeholder='Cari Laporan Publik'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "100%", md: "30%" }}
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Group justify="space-between">
|
||||
@@ -115,7 +118,7 @@ function Page() {
|
||||
return (
|
||||
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
|
||||
<Stack>
|
||||
<Title c={colors['blue-button']} order={1}>{v.judul}</Title>
|
||||
<Text c={colors['blue-button']} lineClamp={3} truncate="end" fz="h4" fw="bold">{v.judul}</Text>
|
||||
<Text fs={'italic'} fz={'xl'}>
|
||||
{v.tanggalWaktu
|
||||
? new Date(v.tanggalWaktu).toLocaleString('id-ID')
|
||||
|
||||
@@ -41,6 +41,37 @@ function Page() {
|
||||
)
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||
Pencegahan Kriminalitas
|
||||
</Text>
|
||||
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
|
||||
Keamanan Komunitas & Pencegahan Kriminal
|
||||
</Text>
|
||||
</Box>
|
||||
<SimpleGrid
|
||||
px={{ base: 20, md: 100 }}
|
||||
cols={{ base: 1, md: 2 }}
|
||||
spacing="xl"
|
||||
>
|
||||
<Paper p="xl" radius="xl" shadow="lg" >
|
||||
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
|
||||
Program Keamanan Berjalan
|
||||
</Text>
|
||||
<Stack pt={30} gap="lg">
|
||||
<Text c="dimmed">
|
||||
Tidak ada data pencegahan kriminalitas yang cocok
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
|
||||
@@ -84,9 +84,8 @@ function Page() {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="h4" fw="bold">Kenali Gejala DBD</Text>
|
||||
<Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text>
|
||||
<Divider my="xs" />
|
||||
<Text fz="md" fw="semibold">{state.findUnique.data.symptom?.title}</Text>
|
||||
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||
import colors from '@/con/colors';
|
||||
import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, AspectRatio, Badge, Box, Button, Card, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react';
|
||||
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconUser } from '@tabler/icons-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -149,11 +149,6 @@ function Page() {
|
||||
</CopyButton>
|
||||
</Group>
|
||||
</Group>
|
||||
<Group gap="xs" mt="sm" wrap="wrap">
|
||||
<Chip defaultChecked radius="xl" variant="light" icon={<IconStethoscope size={16} />}>Layanan Medis</Chip>
|
||||
<Chip radius="xl" variant="light" icon={<IconUsersGroup size={16} />}>Ramah Keluarga</Chip>
|
||||
<Chip radius="xl" variant="light" icon={<IconWallet size={16} />}>Pembayaran Non-Tunai</Chip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
@@ -210,7 +205,6 @@ function Page() {
|
||||
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">WhatsApp</Button>
|
||||
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
|
||||
</Group>
|
||||
<Anchor target="_blank" underline="hover">Kunjungi situs resmi</Anchor>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
@@ -246,15 +240,8 @@ function Page() {
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Grid gutter="lg">
|
||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||
<Card radius="xl" p="lg" withBorder>
|
||||
<Card radius="xl" p="lg" withBorder>
|
||||
<Stack gap="md">
|
||||
<Title order={3}>Fasilitas Pendukung</Title>
|
||||
<Divider />
|
||||
@@ -270,8 +257,7 @@ function Page() {
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||
|
||||
<Card radius="xl" p="lg" withBorder>
|
||||
<Stack gap="md">
|
||||
<Title order={3}>Layanan & Tarif</Title>
|
||||
@@ -309,10 +295,11 @@ function Page() {
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }} pb="xl">
|
||||
<Paper radius="xl" p="lg" withBorder>
|
||||
<Stack gap="md">
|
||||
|
||||
@@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page';
|
||||
function Page() {
|
||||
const params = useParams();
|
||||
const state = useProxy(jadwalkegiatanState);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findUnique.load(params?.id as string);
|
||||
@@ -66,28 +69,38 @@ function Page() {
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
|
||||
<Divider />
|
||||
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
|
||||
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
|
||||
<Divider />
|
||||
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
|
||||
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
|
||||
<Divider />
|
||||
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
|
||||
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
|
||||
<Divider />
|
||||
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
|
||||
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
|
||||
</Stack>
|
||||
|
||||
<CreatePendaftaran />
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Pendaftaran Kegiatan</Text>
|
||||
<Divider />
|
||||
<Group>
|
||||
<Button onClick={open}>Buat Pendaftaran</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Modal opened={opened} onClose={close}>
|
||||
<CreatePendaftaran />
|
||||
</Modal>
|
||||
|
||||
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
|
||||
<Stack gap="xs">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import colors from '@/con/colors';
|
||||
import { BarChart as MantineBarChart } from '@mantine/charts';
|
||||
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
@@ -107,20 +107,7 @@ function Page() {
|
||||
<Box>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||
<Box pb={30}>
|
||||
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
||||
<ColorSwatch color="#3290CA" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
|
||||
{chartData.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
Belum ada data yang tersedia untuk ditampilkan
|
||||
@@ -150,6 +137,20 @@ function Page() {
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
||||
<ColorSwatch color="#3290CA" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
@@ -163,11 +164,11 @@ function Page() {
|
||||
}}
|
||||
>
|
||||
{/* Fasilitas Kesehatan */}
|
||||
<FasilitasKesehatan/>
|
||||
<FasilitasKesehatan />
|
||||
{/* Jadwal Kegiatan */}
|
||||
<JadwalKegiatan/>
|
||||
<JadwalKegiatan />
|
||||
{/* Artikel Kesehatan */}
|
||||
<ArtikelKesehatanPage/>
|
||||
<ArtikelKesehatanPage />
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -121,12 +121,15 @@ function Page() {
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Diposting: {v.createdAt.toLocaleDateString()}
|
||||
Diposting: {new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
|
||||
{v.deskripsiSingkat}
|
||||
</Text>
|
||||
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end" dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }} />
|
||||
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
|
||||
Selengkapnya
|
||||
</Button>
|
||||
|
||||
@@ -101,27 +101,42 @@ function Page() {
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
w={140}
|
||||
h={140}
|
||||
fit="contain"
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
|
||||
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconBrandWhatsapp size={18} />}
|
||||
component="a"
|
||||
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
|
||||
target="_blank"
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconBrandWhatsapp size={18} />}
|
||||
component="a"
|
||||
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
|
||||
target="_blank"
|
||||
aria-label="Hubungi WhatsApp"
|
||||
>WhatsApp</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Center,
|
||||
Grid,
|
||||
@@ -106,15 +105,34 @@ function Page() {
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
<Center>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
h={180}
|
||||
w="100%"
|
||||
radius="md"
|
||||
fit="cover"
|
||||
style={{ aspectRatio: '4/3' }}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 180, // 🔥 tinggi fix biar semua seragam
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image?.link || '/img/default.png'}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
width="100%"
|
||||
height="100%"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
</Center>
|
||||
<Stack gap={4} w="100%">
|
||||
<Text
|
||||
@@ -136,9 +154,6 @@ function Page() {
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Badge radius="md" color="blue" variant="light" mt="sm">
|
||||
Darurat
|
||||
</Badge>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
@@ -160,7 +175,7 @@ function Page() {
|
||||
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
|
||||
},
|
||||
}}
|
||||
|
||||
|
||||
/>
|
||||
</Center>
|
||||
|
||||
|
||||
120
src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx
Normal file
120
src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
|
||||
|
||||
export default function DetailPosyanduUser() {
|
||||
const statePosyandu = useProxy(posyanduState);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
statePosyandu.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
if (!statePosyandu.findUnique.data) {
|
||||
return (
|
||||
<Stack py="xl" px={{ base: 'md', md: 100 }}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = statePosyandu.findUnique.data;
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl">
|
||||
{/* Tombol Kembali */}
|
||||
<Group>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
|
||||
mb="sm"
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
bg={colors['white-trans-1']}
|
||||
maw={800}
|
||||
mx="auto"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: '1.8rem', md: '2.2rem' }}
|
||||
fw={700}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
{data.name || 'Posyandu Desa'}
|
||||
</Text>
|
||||
|
||||
{/* Gambar */}
|
||||
{data.image?.link ? (
|
||||
<Center>
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={`Gambar ${data.name}`}
|
||||
w="100%"
|
||||
h={300}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Center>
|
||||
) : (
|
||||
<Center>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Tidak ada gambar
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* Info utama */}
|
||||
<Stack gap="sm" mt="md">
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text fz="sm" c="dimmed">
|
||||
{data.nomor || 'Nomor tidak tersedia'}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
lh={1.6}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
'use client'
|
||||
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
|
||||
import colors from "@/con/colors";
|
||||
import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import BackButton from "../../desa/layanan/_com/BackButto";
|
||||
import { useTransitionRouter } from "next-view-transitions";
|
||||
|
||||
export default function Page() {
|
||||
const state = useProxy(posyandustate);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||
const router = useTransitionRouter()
|
||||
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 6, search);
|
||||
}, [page, search]);
|
||||
load(page, 6, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -131,33 +134,41 @@ export default function Page() {
|
||||
loading="lazy"
|
||||
/>
|
||||
</Center>
|
||||
<Flex align="center" gap="xs">
|
||||
<IconPhone size={18} stroke={1.5} />
|
||||
<Text fz="sm" c="dimmed">
|
||||
{v.nomor || "Tidak tersedia"}
|
||||
</Text>
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Box>
|
||||
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||
{v.nomor || "Tidak tersedia"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex align="center" gap="xs">
|
||||
<IconCalendar size={18} stroke={1.5} />
|
||||
<Text fz="sm" c="dimmed">
|
||||
Jadwal:{" "}
|
||||
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} />
|
||||
</Text>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Box>
|
||||
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||
<strong>Jadwal:</strong>{" "}
|
||||
<span
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
|
||||
/>
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Spoiler
|
||||
key={`spoiler-${v.id}`}
|
||||
maxHeight={70}
|
||||
showLabel="Lihat selengkapnya"
|
||||
hideLabel="Sembunyikan"
|
||||
transitionDuration={300}
|
||||
>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
lineClamp={3}
|
||||
truncate="end"
|
||||
/>
|
||||
</Spoiler>
|
||||
</Flex>
|
||||
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>Detail</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
|
||||
@@ -126,17 +126,36 @@ export default function Page() {
|
||||
className="hover-scale"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box h={180} w="100%">
|
||||
<Image
|
||||
src={v.image?.link}
|
||||
alt={v.name}
|
||||
radius="xl"
|
||||
w="100%"
|
||||
h="100%"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 180, // 🔥 tinggi fix biar semua seragam
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image?.link || '/img/default.png'}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
width="100%"
|
||||
height="100%"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
</Center>
|
||||
|
||||
<Box px="lg" pb="lg">
|
||||
<Text
|
||||
|
||||
@@ -49,12 +49,10 @@ function Page() {
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
|
||||
<Tooltip label={<Text fz={"sm"} c={"white"} dangerouslySetInnerHTML={{ __html: tujuan.data?.judul || '' }} /> } position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
<IconLeaf size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{tujuan.data?.judul}
|
||||
</Text>
|
||||
<Text dangerouslySetInnerHTML={{ __html: tujuan.data?.judul || '' }} fz="h3" fw="bold" c={colors['blue-button']} ta="center" />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -78,9 +76,7 @@ function Page() {
|
||||
<Tooltip label={materi.data?.judul} position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
<IconRecycle size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{materi.data?.judul}
|
||||
</Text>
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center" dangerouslySetInnerHTML={{ __html: materi.data?.judul || '' }} />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Transition } from '@mantine/core';
|
||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||
import {
|
||||
Badge,
|
||||
@@ -23,12 +27,11 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
export default function Content({ kategori }: { kategori: string }) {
|
||||
const router = useTransitionRouter();
|
||||
const [page, setPage] = useState(1);
|
||||
const [animateKey, setAnimateKey] = useState(0);
|
||||
|
||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||
const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
|
||||
@@ -37,119 +40,178 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
const totalPages = state.findMany.totalPages || 1;
|
||||
|
||||
// Load data
|
||||
// Load data awal
|
||||
useEffect(() => {
|
||||
gotongRoyongState.kegiatanDesa.findFirst.load(kategori);
|
||||
}, [kategori]);
|
||||
|
||||
// Load daftar berita
|
||||
useEffect(() => {
|
||||
state.findMany.load(page, 3, '', kategori);
|
||||
setAnimateKey((prev) => prev + 1); // trigger animasi halus saat page berubah
|
||||
}, [page, kategori]);
|
||||
|
||||
// Tampilan kosong
|
||||
if (!featuredState.loading && !featured) {
|
||||
return (
|
||||
<Center py={100}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={3}>Belum Ada Data Gotong Royong</Title>
|
||||
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={20}>
|
||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||
{/* === Gotong Royong Utama === */}
|
||||
{featuredState.loading ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : featured ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featured.image?.link}
|
||||
alt={featured.judul || 'Berita Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featured.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : null}
|
||||
<Transition mounted={!featuredState.loading} transition="fade" duration={250} timingFunction="ease">
|
||||
{(styles) => (
|
||||
<div style={styles}>
|
||||
{featured ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featured.image?.link || '/images/placeholder.jpg'}
|
||||
alt={featured.judul || 'Berita Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featured.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mb="md"
|
||||
dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }}
|
||||
/>
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<Skeleton h={400} radius="md" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
{/* === Daftar Gotong Royong === */}
|
||||
{/* === Daftar Gotong Royong (Pagination + Fade-in Halus) === */}
|
||||
<Box mt={50}>
|
||||
<Title order={2} mb="md">Daftar Gotong Royong</Title>
|
||||
<Divider mb="xl" />
|
||||
|
||||
{state.findMany.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3).fill(0).map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">Belum ada gotong royong di kategori "{kategori}".</Text>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card.Section>
|
||||
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
|
||||
</Card.Section>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={animateKey}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.25, ease: 'easeInOut' }}
|
||||
>
|
||||
{state.findMany.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Center py={50}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={3}>Tidak Ada Data</Title>
|
||||
<Text c="dimmed">Belum ada data gotong royong yang tersedia.</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
mt="xs"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }}
|
||||
/>
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center mt="xl">
|
||||
@@ -166,4 +228,4 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,169 +1,277 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||
import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Image,
|
||||
Pagination,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Transition,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useTransitionRouter();
|
||||
const router = useRouter();
|
||||
|
||||
// Parameter URL
|
||||
const search = searchParams.get('search') || '';
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
|
||||
// Gunakan proxy untuk state
|
||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst); // ✅ Berita utama
|
||||
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
|
||||
const loadingGrid = state.findMany.loading;
|
||||
const loadingFeatured = featured.loading;
|
||||
|
||||
// Load berita utama (hanya sekali)
|
||||
// Load featured data once on component mount
|
||||
useEffect(() => {
|
||||
if (!featured.data && !loadingFeatured) {
|
||||
gotongRoyongState.kegiatanDesa.findFirst.load();
|
||||
}
|
||||
}, [featured.data, loadingFeatured]);
|
||||
let mounted = true;
|
||||
|
||||
const loadFeatured = async () => {
|
||||
try {
|
||||
if (!featured.data && !loadingFeatured) {
|
||||
await gotongRoyongState.kegiatanDesa.findFirst.load();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading featured data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (mounted) {
|
||||
loadFeatured();
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []); // Empty dependency array to run only once on mount
|
||||
|
||||
// Load berita terbaru (untuk grid) saat page/search berubah
|
||||
useEffect(() => {
|
||||
const limit = 3; // Sesuaikan dengan tampilan grid
|
||||
state.findMany.load(page, limit, search);
|
||||
let mounted = true;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const limit = 3;
|
||||
await state.findMany.load(page, limit, search);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (mounted) {
|
||||
loadData();
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [page, search]);
|
||||
|
||||
// Update URL saat page berubah
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const url = new URLSearchParams(searchParams.toString());
|
||||
if (search) url.set('search', search);
|
||||
if (newPage > 1) url.set('page', newPage.toString());
|
||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
||||
|
||||
router.replace(`?${url.toString()}`);
|
||||
else url.delete('page');
|
||||
|
||||
// Use push instead of replace to keep browser history
|
||||
router.push(`?${url.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const featuredData = featured.data;
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
const totalPages = state.findMany.totalPages || 1;
|
||||
|
||||
return (
|
||||
<Box py={20}>
|
||||
<Container size="xl" px={{ base: "md", md: "xl" }}>
|
||||
{/* === Gotong royong Utama (Tetap) === */}
|
||||
{loadingFeatured ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : featuredData ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featuredData.image?.link || '/images/placeholder.jpg'}
|
||||
alt={featuredData.judul || 'Gotong royong Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`)}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : null}
|
||||
// Animasi transisi halus tapi tetap instant load
|
||||
const MotionBox = motion(Box as any);
|
||||
|
||||
{/* === Gotong royong Terbaru (Berubah Saat Pagination) === */}
|
||||
// fallback kosong
|
||||
if (!loadingGrid && !loadingFeatured && paginatedNews.length === 0) {
|
||||
return (
|
||||
<Container size="xl" py={80} ta="center">
|
||||
<Title order={2} mb="md">Belum Ada Data Gotong Royong</Title>
|
||||
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
key={`${page}-${search}`}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
py={20}
|
||||
>
|
||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||
{/* === Gotong Royong Utama === */}
|
||||
<Transition mounted={!loadingFeatured} transition="fade" duration={200} timingFunction="ease-out">
|
||||
{(styles) =>
|
||||
featuredData ? (
|
||||
<Box mb={50} style={styles}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featuredData.image?.link || '/images/placeholder.jpg'}
|
||||
alt={featuredData.judul || 'Gotong royong Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mb="md"
|
||||
dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }}
|
||||
/>
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<Skeleton h={400} radius="md" mb="xl" />
|
||||
)
|
||||
}
|
||||
</Transition>
|
||||
|
||||
{/* === Gotong royong Terbaru === */}
|
||||
<Box mt={50}>
|
||||
<Title order={2} mb="md">Gotong royong Terbaru</Title>
|
||||
<Divider mb="xl" />
|
||||
|
||||
{loadingGrid ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3).fill(0).map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">Tidak ada gotong royong ditemukan.</Text>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Transition mounted={!loadingGrid} transition="fade" duration={200} timingFunction="ease-out">
|
||||
{(styles) =>
|
||||
loadingGrid ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">
|
||||
Tidak ada gotong royong ditemukan.
|
||||
</Text>
|
||||
) : (
|
||||
<Box style={styles}>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mt="xs"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
|
||||
/>
|
||||
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`)}>Baca Selengkapnya</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{/* Pagination hanya untuk berita terbaru */}
|
||||
<Button
|
||||
p="xs"
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Transition>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
@@ -176,9 +284,6 @@ function Page() {
|
||||
</Center>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</MotionBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
|
||||
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconRoute, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
@@ -122,20 +122,28 @@ function Page() {
|
||||
<Stack gap="md">
|
||||
{data2?.map((v, k) => (
|
||||
<Paper key={k} p="md" withBorder radius="md">
|
||||
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
|
||||
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
|
||||
{v.lat && v.lng ? (
|
||||
<a
|
||||
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: colors['blue-button'], textDecoration: 'none' }}
|
||||
>
|
||||
<Text fz="sm">📌 Buka di Google Maps</Text>
|
||||
</a>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
|
||||
)}
|
||||
<Group justify='space-between'>
|
||||
<Box>
|
||||
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
|
||||
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconRoute color={colors['blue-button']} size={30} />
|
||||
<Text fw={"bold"} fz="sm" c={colors['blue-button']}>Rute</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
{v.lat && v.lng ? (
|
||||
<a
|
||||
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: colors['blue-button'], textDecoration: 'none' }}
|
||||
>
|
||||
<Text fz="sm">📌 Lihat Peta Lebih Besar</Text>
|
||||
</a>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
|
||||
)}
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -74,7 +74,7 @@ function Page() {
|
||||
<Title fz={55} fw={900} c={colors['blue-button']}>
|
||||
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
|
||||
</Title>
|
||||
<Text fz="lg" mt="md" c="dimmed">
|
||||
<Text fz="lg" mt="md" fw={"bold"}>
|
||||
Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
|
||||
</Text>
|
||||
<Group mt="xl">
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Timeline,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { IconArrowLeft, IconChecklist, IconInfoCircle, IconQuote, IconSchool, IconTimeline, IconUserPlus } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -69,10 +69,11 @@ export default function BeasiswaPage() {
|
||||
{/* Hero Section */}
|
||||
<Container size="lg" py="xl">
|
||||
<Stack gap="md" maw={600}>
|
||||
<Title order={2} c="blue.9">
|
||||
Program Beasiswa Pendidikan Desa Darmasaba
|
||||
</Title>
|
||||
<Text c="dimmed">
|
||||
<Group>
|
||||
<IconSchool size={30} color={colors["blue-button"]} />
|
||||
<Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
|
||||
</Group>
|
||||
<Text>
|
||||
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
|
||||
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
|
||||
</Text>
|
||||
@@ -81,21 +82,35 @@ export default function BeasiswaPage() {
|
||||
|
||||
{/* Tentang Program */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Tentang Program
|
||||
</Title>
|
||||
<Group mb="sm">
|
||||
<IconInfoCircle size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Tentang Program</Title>
|
||||
</Group>
|
||||
<Text>
|
||||
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
|
||||
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
|
||||
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
|
||||
</Text>
|
||||
|
||||
{/* Tambahkan info tahun berjalan di sini */}
|
||||
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
|
||||
<Text fw={500} c={colors["blue-button"]}>
|
||||
📅 Periode Beasiswa Tahun 2025
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada
|
||||
<strong>31 Mei 2025</strong>.
|
||||
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
|
||||
</Text>
|
||||
</Paper>
|
||||
</Container>
|
||||
|
||||
{/* Syarat dan Ketentuan */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Syarat Pendaftaran
|
||||
</Title>
|
||||
<Group mb="sm">
|
||||
<IconChecklist size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Syarat Pendaftaran</Title>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||
<Paper shadow="sm" p="md" radius="lg" withBorder>
|
||||
@@ -123,42 +138,61 @@ export default function BeasiswaPage() {
|
||||
|
||||
{/* Proses Seleksi */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Proses Seleksi
|
||||
</Title>
|
||||
<Group mb="sm">
|
||||
<IconTimeline size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Proses Seleksi</Title>
|
||||
</Group>
|
||||
|
||||
<Timeline active={4} bulletSize={24} lineWidth={2}>
|
||||
<Timeline.Item title="Pendaftaran Online">
|
||||
<Text c="dimmed" size="sm">
|
||||
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 1 Februari – 31 Mei 2025
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
<Timeline.Item title="Seleksi Administrasi">
|
||||
<Text c="dimmed" size="sm">
|
||||
Panitia memverifikasi kelengkapan dan validitas berkas.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 5–7 hari kerja setelah penutupan pendaftaran
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
<Timeline.Item title="Wawancara dan Penilaian">
|
||||
<Text c="dimmed" size="sm">
|
||||
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 7–10 hari kerja setelah pengumuman seleksi administrasi
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
<Timeline.Item title="Pengumuman Penerima">
|
||||
<Text c="dimmed" size="sm">
|
||||
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
|
||||
<Text c="dimmed" size="sm" mt="lg" ta="center">
|
||||
🗓️ Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
|
||||
{/* Testimoni */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Cerita Sukses Penerima Beasiswa
|
||||
</Title>
|
||||
<Group mb="sm">
|
||||
<IconQuote size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<Paper shadow="md" p="lg" radius="lg">
|
||||
@@ -183,7 +217,10 @@ export default function BeasiswaPage() {
|
||||
|
||||
{/* CTA Akhir */}
|
||||
<Container size="lg" py="xl" ta="center">
|
||||
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
|
||||
<Group justify="center" mb="sm">
|
||||
<IconUserPlus size={28} color={colors["blue-button"]} />
|
||||
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
|
||||
</Group>
|
||||
<Text c="dimmed" mb="md">
|
||||
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
|
||||
</Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge, Group } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
|
||||
@@ -49,46 +49,46 @@ function Page() {
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
|
||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateTujuanProgram.findById.data?.judul}
|
||||
</Badge>
|
||||
<Group>
|
||||
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
|
||||
<Box>
|
||||
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateTujuanProgram.findById.data?.judul}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateLokasiDanJadwal.findById.data?.judul}
|
||||
</Badge>
|
||||
<Group>
|
||||
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
|
||||
<Box>
|
||||
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateLokasiDanJadwal.findById.data?.judul}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateFasilitas.findById.data?.judul}
|
||||
</Badge>
|
||||
<Group>
|
||||
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
|
||||
<Box>
|
||||
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateFasilitas.findById.data?.judul}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -92,7 +92,7 @@ function Page() {
|
||||
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} />
|
||||
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Penduduk dengan Pendidikan" radius={[8, 8, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
|
||||
@@ -84,7 +84,7 @@ function Page({ params }: PageProps) {
|
||||
<TableTr>
|
||||
<TableTh w="30%">Nama Pengajar</TableTh>
|
||||
<TableTh w="30%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
<TableTh w="40%">Mengajar Di Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -95,7 +95,7 @@ function Page({ params }: PageProps) {
|
||||
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { Box, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
|
||||
@@ -43,7 +43,7 @@ function Page() {
|
||||
Pendidikan Non Formal
|
||||
</Title>
|
||||
<Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
|
||||
Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang.
|
||||
Pendidikan non formal merupakan bentuk pendidikan di luar sekolah yang terstruktur, bertujuan untuk memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang.
|
||||
</Text>
|
||||
</Box>
|
||||
<SimpleGrid
|
||||
@@ -59,13 +59,17 @@ function Page() {
|
||||
withBorder
|
||||
>
|
||||
<Stack>
|
||||
<Tooltip label="Fokus utama program" withArrow>
|
||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
||||
<IconTarget size={28} style={{ marginRight: 8 }} />
|
||||
<Group align="center" gap={8} wrap="nowrap">
|
||||
<Tooltip label="Fokus utama program" withArrow>
|
||||
<Box display="flex" style={{ alignItems: "center" }}>
|
||||
<IconTarget color={colors['blue-button']} size={26} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||
{stateTujuanPendidikanNonFormal.findById.data?.judul}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper
|
||||
@@ -76,13 +80,17 @@ function Page() {
|
||||
withBorder
|
||||
>
|
||||
<Stack>
|
||||
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
|
||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
||||
<IconMapPin size={28} style={{ marginRight: 8 }} />
|
||||
<Group align="center" gap={8} wrap="nowrap">
|
||||
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
|
||||
<Box display="flex" style={{ alignItems: "center" }}>
|
||||
<IconMapPin color={colors['blue-button']} size={26} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||
{stateTempatKegiatan.findById.data?.judul}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
@@ -95,13 +103,17 @@ function Page() {
|
||||
withBorder
|
||||
>
|
||||
<Stack>
|
||||
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
|
||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
||||
<IconBook2 size={28} style={{ marginRight: 8 }} />
|
||||
<Group align="center" gap={8} wrap="nowrap">
|
||||
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
|
||||
<Box display="flex" style={{ alignItems: "center" }}>
|
||||
<IconBook2 color={colors['blue-button']} size={26} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||
{stateJenisProgram.findById.data?.judul}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -16,16 +16,11 @@ import {
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconBook2,
|
||||
IconUser
|
||||
} from '@tabler/icons-react';
|
||||
import { IconArrowRight, IconBook2, IconUser } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
||||
export interface ModalPeminjamanProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
@@ -45,11 +40,12 @@ export default function ModalPeminjaman({
|
||||
}: ModalPeminjamanProps) {
|
||||
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
|
||||
|
||||
// reset form setiap modal dibuka
|
||||
const BATAS_HARI_PINJAM = 4;
|
||||
|
||||
// Reset form setiap modal dibuka
|
||||
useEffect(() => {
|
||||
if (opened && buku) {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form = {
|
||||
...perpustakaanDigitalState.peminjamanBuku.create.form,
|
||||
bukuId: buku.id,
|
||||
nama: '',
|
||||
noTelp: '',
|
||||
@@ -99,7 +95,14 @@ export default function ModalPeminjaman({
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Text fz="sm" c="dimmed" lineClamp={3} dangerouslySetInnerHTML={{ __html: buku.deskripsi || 'Tidak ada deskripsi' }} />
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: buku.deskripsi || 'Tidak ada deskripsi',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
@@ -112,7 +115,8 @@ export default function ModalPeminjaman({
|
||||
leftSection={<IconUser size={16} />}
|
||||
value={snap.create.form.nama}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value)
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.nama =
|
||||
e.currentTarget.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
@@ -123,7 +127,8 @@ export default function ModalPeminjaman({
|
||||
leftSection={<IconUser size={16} />}
|
||||
value={snap.create.form.noTelp}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value)
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp =
|
||||
e.currentTarget.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
@@ -134,11 +139,13 @@ export default function ModalPeminjaman({
|
||||
leftSection={<IconUser size={16} />}
|
||||
value={snap.create.form.alamat}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value)
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat =
|
||||
e.currentTarget.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */}
|
||||
<DateInput
|
||||
label="Tanggal Pinjam"
|
||||
placeholder="Pilih tanggal pinjam"
|
||||
@@ -148,64 +155,83 @@ export default function ModalPeminjaman({
|
||||
: null
|
||||
}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
if (date) {
|
||||
const tanggalPinjam = new Date(date);
|
||||
|
||||
// simpan tanggal pinjam
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
|
||||
tanggalPinjam.toISOString();
|
||||
|
||||
// hitung batas +4 hari
|
||||
const batasKembali = new Date(tanggalPinjam);
|
||||
batasKembali.setDate(batasKembali.getDate() + BATAS_HARI_PINJAM);
|
||||
|
||||
// set batas & tanggal kembali otomatis
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
|
||||
batasKembali.toISOString();
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
|
||||
batasKembali.toISOString();
|
||||
|
||||
toast.info(
|
||||
`Batas pengembalian otomatis diset ke ${batasKembali.toLocaleDateString('id-ID')} (+${BATAS_HARI_PINJAM} hari).`
|
||||
);
|
||||
} else {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = '';
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = '';
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = '';
|
||||
}
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text>Catatan</Text>
|
||||
<Text fw={500}>Catatan</Text>
|
||||
<CreateEditor
|
||||
value={snap.create.form.catatan}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e)
|
||||
onChange={(val) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan =
|
||||
val)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<DateInput
|
||||
label="Tanggal Kembali"
|
||||
placeholder="Pilih tanggal kembali"
|
||||
value={
|
||||
snap.create.form.tanggalKembali
|
||||
? new Date(snap.create.form.tanggalKembali)
|
||||
: null
|
||||
}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label="Batas Pengembalian"
|
||||
placeholder="Pilih tanggal kembali"
|
||||
placeholder="Otomatis diatur +4 hari dari tanggal pinjam"
|
||||
value={
|
||||
snap.create.form.batasKembali
|
||||
? new Date(snap.create.form.batasKembali)
|
||||
: null
|
||||
}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
required
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label="Tanggal Kembali"
|
||||
placeholder="Otomatis sama dengan batas pengembalian"
|
||||
value={
|
||||
snap.create.form.tanggalKembali
|
||||
? new Date(snap.create.form.tanggalKembali)
|
||||
: null
|
||||
}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
loading={snap.create.loading}
|
||||
disabled={
|
||||
!snap.create.form.nama ||
|
||||
!snap.create.form.tanggalPinjam ||
|
||||
!snap.create.form.batasKembali ||
|
||||
!snap.create.form.tanggalKembali
|
||||
!snap.create.form.nama || !snap.create.form.tanggalPinjam
|
||||
}
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
radius="xl"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Pinjam Buku
|
||||
</Button>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Content() {
|
||||
try {
|
||||
await state.dataPerpustakaan.findMany.load(
|
||||
currentPage,
|
||||
10,
|
||||
3,
|
||||
searchQuery,
|
||||
''
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ function Page() {
|
||||
>
|
||||
Dasar Hukum
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="dimmed">
|
||||
<Text ta="center" fz="md" >
|
||||
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
|
||||
import colors from "@/con/colors";
|
||||
import { BarChart, PieChart } from '@mantine/charts';
|
||||
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
|
||||
import { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { useState } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
|
||||
@@ -18,14 +18,13 @@ interface ChartDataItem {
|
||||
|
||||
|
||||
function Kepuasan() {
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const { data, loading } = state.findMany;
|
||||
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
||||
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
||||
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
|
||||
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
|
||||
const resetForm = () => {
|
||||
state.create.form = {
|
||||
@@ -122,18 +121,18 @@ function Kepuasan() {
|
||||
|
||||
// Convert map to array and sort by date
|
||||
const barData = Array.from(monthYearMap.entries())
|
||||
.map(([key, count]) => {
|
||||
.map(([key, Responden]) => {
|
||||
const [year, month] = key.split('-');
|
||||
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
||||
.toLocaleString('id-ID', { month: 'long' });
|
||||
return {
|
||||
month: `${monthName} ${year}`,
|
||||
count,
|
||||
Responden,
|
||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.sortKey - b.sortKey)
|
||||
.map(({ month, count }) => ({ month, count }));
|
||||
.map(({ month, Responden }) => ({ month, Responden }));
|
||||
|
||||
setBarChartData(barData);
|
||||
}
|
||||
@@ -141,12 +140,12 @@ function Kepuasan() {
|
||||
|
||||
if ((loading && !data) || !data) {
|
||||
return (
|
||||
<Stack py={10} px="sm">
|
||||
<Skeleton height={200} mb="md" />
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md">
|
||||
<Skeleton height={200} />
|
||||
<Skeleton height={200} />
|
||||
<Skeleton height={200} />
|
||||
<Stack py={10} px="xl">
|
||||
<Skeleton height={300} mb="md" />
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
<Skeleton height={300} />
|
||||
<Skeleton height={300} />
|
||||
<Skeleton height={300} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
@@ -157,10 +156,16 @@ function Kepuasan() {
|
||||
<Stack p="sm">
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
||||
<Center>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
</Center>
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
|
||||
<Center mt={10}>
|
||||
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||
<Button
|
||||
radius={"lg"}
|
||||
onClick={open}
|
||||
variant="gradient"
|
||||
gradient={{ from: "#26667F", to: "#124170" }}
|
||||
>Ajukan Responden</Button>
|
||||
</Center>
|
||||
</Container>
|
||||
<Box px={"xl"}>
|
||||
@@ -177,10 +182,10 @@ function Kepuasan() {
|
||||
</Box>
|
||||
</Flex>
|
||||
<BarChart
|
||||
h={300}
|
||||
h={window.innerWidth < 480 ? 200 : 300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
||||
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
@@ -191,12 +196,9 @@ function Kepuasan() {
|
||||
</Paper>
|
||||
<Box py={"xl"}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 3
|
||||
}}
|
||||
cols={{ base: 1, sm: 2, lg: 3 }}
|
||||
spacing="md"
|
||||
verticalSpacing="md"
|
||||
>
|
||||
{/* Chart Jenis Kelamin */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
@@ -215,7 +217,7 @@ function Kepuasan() {
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={200}
|
||||
size={250} // Fixed size in pixels
|
||||
data={donutDataJenisKelamin}
|
||||
/>
|
||||
</Center>
|
||||
@@ -254,7 +256,7 @@ function Kepuasan() {
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={200}
|
||||
size={250}
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
@@ -297,7 +299,7 @@ function Kepuasan() {
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={190}
|
||||
size={250}
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
@@ -330,7 +332,7 @@ function Kepuasan() {
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
defaultValue={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.currentTarget.value;
|
||||
@@ -413,41 +415,57 @@ function Kepuasan() {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack p="sm">
|
||||
<Container w={{ base: "100%", md: "80%" }} p={isMobile ? "md" : "xl"}>
|
||||
<Stack gap="xs">
|
||||
<Text ta="center" fz={{ base: "2rem", md: "3rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
<Group justify="center">
|
||||
<Button radius="lg" bg={colors["blue-button"]} onClick={open}>
|
||||
Ajukan Responden
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Stack p={"sm"}>
|
||||
<Container size="lg" px="md">
|
||||
<Center>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
</Center>
|
||||
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
|
||||
<Center mt={10}>
|
||||
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||
</Center>
|
||||
</Container>
|
||||
<Box px={isMobile ? "sm" : "xl"}>
|
||||
<Paper p="lg" bg={colors.Bg}>
|
||||
<Paper p={isMobile ? "sm" : "lg"}>
|
||||
<Stack gap="xs">
|
||||
<Flex direction={isMobile ? "column" : "row"} justify="space-between" align={isMobile ? "start" : "center"}>
|
||||
<Text fw="bold" mb={isMobile ? "sm" : 0}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" c={colors["blue-button"]}>Total Responden</Text>
|
||||
<Text ta="end" fz="h1" fw="bold" c={colors["blue-button"]}>
|
||||
{state.findMany.total.toLocaleString("id-ID")}
|
||||
<Box px={"xl"}>
|
||||
<Paper p={"lg"} bg={colors.Bg}>
|
||||
<Paper p={"lg"}>
|
||||
<Stack gap={"xs"}>
|
||||
<Flex
|
||||
direction={{ base: "column", sm: "row" }}
|
||||
justify="space-between"
|
||||
align={{ base: "flex-start", sm: "center" }}
|
||||
>
|
||||
<Text fw="bold" ta={{ base: "center", sm: "left" }}>
|
||||
Pelayanan Terhadap Publik Desa Darmasaba
|
||||
</Text>
|
||||
<Box mt={{ base: "sm", sm: 0 }}>
|
||||
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
|
||||
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
|
||||
{state.findMany.total.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<BarChart
|
||||
h={isMobile ? 200 : 300}
|
||||
h={300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: "count", color: colors["blue-button"] }]}
|
||||
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box py="xl">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg">
|
||||
<Box py={"xl"}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 3
|
||||
}}
|
||||
>
|
||||
{/* Chart Jenis Kelamin */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
@@ -457,17 +475,28 @@ function Kepuasan() {
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md">
|
||||
<Stack>
|
||||
<Center>
|
||||
<PieChart
|
||||
size={isMobile ? 150 : 200}
|
||||
withLabels
|
||||
data={donutDataJenisKelamin}
|
||||
withTooltip
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={200}
|
||||
data={donutDataJenisKelamin}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Stack gap="sm" mt="md">
|
||||
{donutDataJenisKelamin.map((entry) => (
|
||||
<Flex key={entry.name} gap="md" align="center">
|
||||
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
|
||||
<Text size="sm">{entry.name}: {entry.value}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -482,18 +511,35 @@ function Kepuasan() {
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md">
|
||||
<Stack>
|
||||
<Center>
|
||||
<PieChart
|
||||
size={isMobile ? 150 : 200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
withLabelsLine
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={200}
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box mt="md" style={{ width: '100%' }}>
|
||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||
{donutDataRating.map((entry) => (
|
||||
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
|
||||
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{entry.name}: {entry.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -508,18 +554,35 @@ function Kepuasan() {
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md">
|
||||
<Stack>
|
||||
<Center>
|
||||
<PieChart
|
||||
size={isMobile ? 150 : 200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
withLabelsLine
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={190}
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box mt="md" style={{ width: '100%' }}>
|
||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||
{donutDataKelompokUmur.map((entry) => (
|
||||
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
|
||||
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{entry.name}: {entry.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -542,7 +605,7 @@ function Kepuasan() {
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
label="Tanggal Pengisian"
|
||||
type="date"
|
||||
placeholder="masukkan tanggal"
|
||||
defaultValue={state.create.form.tanggal}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function JenisInformasiSelector({ onChange }: {
|
||||
return (
|
||||
<Group>
|
||||
<Select
|
||||
placeholder='pilih jenis informasi'
|
||||
placeholder='Pilih jenis informasi'
|
||||
label='Jenis Informasi'
|
||||
data={data.map((item) => ({
|
||||
value: item.id,
|
||||
|
||||
@@ -28,7 +28,7 @@ function MemperolehInformasi({ onChange }: {
|
||||
return (
|
||||
<Group>
|
||||
<Select
|
||||
placeholder='pilih cara memperoleh informasi'
|
||||
placeholder='Pilih cara memperoleh informasi'
|
||||
label={"Cara Memperoleh Informasi"}
|
||||
data={data.map((item) => ({
|
||||
value: item.id,
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
TextInput
|
||||
} from '@mantine/core';
|
||||
import { IconDownload, IconSend2 } from '@tabler/icons-react';
|
||||
import { IconSend2 } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
@@ -150,23 +149,6 @@ function Page() {
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center pb={30}>
|
||||
<Tooltip label="Unduh dokumen tata cara permohonan" withArrow>
|
||||
<Button
|
||||
fz="sm"
|
||||
size="md"
|
||||
radius="md"
|
||||
bg={colors['blue-button']}
|
||||
leftSection={
|
||||
<IconDownload size={20} color={colors['white-1']} />
|
||||
}
|
||||
>
|
||||
Unduh Tata Cara
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Center>
|
||||
|
||||
<Group justify="center">
|
||||
<Paper
|
||||
p="xl"
|
||||
|
||||
@@ -26,7 +26,7 @@ function MemperolehSalinan({ onChange }: {
|
||||
return (
|
||||
<Group>
|
||||
<Select
|
||||
placeholder='pilih cara memperoleh salinan informasi'
|
||||
placeholder='Pilih cara memperoleh salinan informasi'
|
||||
label={'Cara Memperoleh Salinan Informasi'}
|
||||
data={data.map((item) => ({
|
||||
value: item.id,
|
||||
|
||||
@@ -178,7 +178,7 @@ function Page() {
|
||||
|
||||
<TextInput
|
||||
label="Alamat Email"
|
||||
placeholder="contoh: nama@email.com"
|
||||
placeholder="Contoh: nama@email.com"
|
||||
radius="md"
|
||||
size="md"
|
||||
type="email"
|
||||
@@ -190,7 +190,7 @@ function Page() {
|
||||
|
||||
<TextInput
|
||||
label="Nomor Telepon"
|
||||
placeholder="contoh: 0812-3456-7890"
|
||||
placeholder="Contoh: 0812-3456-7890"
|
||||
radius="md"
|
||||
size="md"
|
||||
withAsterisk
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||
|
||||
function Page() {
|
||||
const allList = useProxy(stateProfilePPID)
|
||||
@@ -36,99 +37,109 @@ function Page() {
|
||||
: [allList.profile.data]
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Profil PPID Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
{dataArray.map((item) => (
|
||||
<Box key={item.id} px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Flex align="center" gap={40} justify="center">
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
|
||||
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
|
||||
Pejabat Pengelola Informasi Publik
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Divider my="lg" />
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} pb={40}>
|
||||
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
|
||||
<Box px={{ base: 0, md: 50 }}>
|
||||
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Center>
|
||||
<Image
|
||||
loading='lazy'
|
||||
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
|
||||
w={{ base: 220, md: 330 }}
|
||||
alt="Foto Pimpinan"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xl">
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconUser size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTimeline size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Box pb={40}>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTargetArrow size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Box>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Profil PPID Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
{dataArray.map((item) => (
|
||||
<Box key={item.id} px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Center>
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} w={{ base: 70, md: 120 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
<Text ta="center" fz={{ base: "1.2rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
|
||||
Pejabat Pengelola Informasi dan Dokumentasi
|
||||
</Text>
|
||||
</Box>
|
||||
<Divider my="lg" />
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} pb={40}>
|
||||
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
|
||||
<Box px={{ base: 0, md: 50 }}>
|
||||
<Paper bg={colors['white-trans-1']} radius="xl" shadow="md" withBorder>
|
||||
<Stack gap={0}>
|
||||
<Image
|
||||
pt={{ base: 0, md: 100 }}
|
||||
px="lg"
|
||||
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
|
||||
alt="Foto Pimpinan"
|
||||
radius="lg"
|
||||
onError={(e) => e.currentTarget.src = "/perbekel.png"}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
px="lg"
|
||||
radius="0 0 var(--mantine-radius-xl) var(--mantine-radius-xl)"
|
||||
className="glass3"
|
||||
py={{ base: 20, md: 50 }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "xl", md: "h2" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xl">
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconUser size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTimeline size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Box pb={40}>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTargetArrow size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
157
src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx
Normal file
157
src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function DetailPegawaiUser() {
|
||||
const statePegawai = useProxy(stateStrukturPPID.pegawai);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||
statePegawai.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
|
||||
if (!statePegawai.findUnique.data) {
|
||||
return (
|
||||
<Stack py="lg">
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = statePegawai.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="xl">
|
||||
{/* Back button */}
|
||||
<Group mb="lg">
|
||||
<Box
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<IconArrowBack size={22} color={colors['blue-button']} />
|
||||
<Text c={colors['blue-button']} fw={500}>
|
||||
Kembali
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
mx="auto"
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
{/* Foto Profil */}
|
||||
<Image
|
||||
src={data.image?.link || '/placeholder-profile.png'}
|
||||
alt={data.namaLengkap || 'Foto Profil'}
|
||||
w={160}
|
||||
h={160}
|
||||
radius={100}
|
||||
fit="cover"
|
||||
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Nama & Jabatan */}
|
||||
<Stack align="center" gap={2}>
|
||||
<Title order={3} fw={700} c={colors['blue-button']}>
|
||||
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||
</Title>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
{/* Informasi Detail */}
|
||||
<Stack gap="md">
|
||||
<InfoRow label="Email" value={data.email} />
|
||||
<InfoRow label="Telepon" value={data.telepon} />
|
||||
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||
<InfoRow
|
||||
label="Tanggal Masuk"
|
||||
value={
|
||||
data.tanggalMasuk
|
||||
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Status"
|
||||
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||
valueColor={data.isActive ? 'green' : 'red'}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/* Komponen kecil untuk menampilkan baris informasi */
|
||||
function InfoRow({
|
||||
label,
|
||||
value,
|
||||
valueColor,
|
||||
multiline = false,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
valueColor?: string;
|
||||
multiline?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} c="dark">
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
c={valueColor || 'dimmed'}
|
||||
style={{
|
||||
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{value || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPegawaiUser;
|
||||
@@ -1,129 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// /* eslint-disable react-hooks/exhaustive-deps */
|
||||
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// 'use client'
|
||||
// import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
// import colors from '@/con/colors';
|
||||
// import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
// import { OrganizationChart } from 'primereact/organizationchart';
|
||||
// import { useEffect } from 'react';
|
||||
// import { useProxy } from 'valtio/utils';
|
||||
// import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
// function Page() {
|
||||
// return (
|
||||
// <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
// <Box px={{ base: 'md', md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
// <Title ta={"center"} fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>Struktur PPID</Title>
|
||||
// <StrukturOrganisasiPPID />
|
||||
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// function StrukturOrganisasiPPID() {
|
||||
// const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
||||
|
||||
// useEffect(() => {
|
||||
// stateOrganisasi.findMany.load()
|
||||
// }, [])
|
||||
|
||||
// if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||
// return (
|
||||
// <Stack py={10}>
|
||||
// <Skeleton h={500} />
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Step 1: Group pegawai berdasarkan posisiId
|
||||
// const posisiMap = new Map<string, any>();
|
||||
|
||||
// for (const pegawai of stateOrganisasi.findMany.data) {
|
||||
// const posisiId = pegawai.posisi.id;
|
||||
// if (!posisiMap.has(posisiId)) {
|
||||
// posisiMap.set(posisiId, {
|
||||
// ...pegawai.posisi,
|
||||
// pegawaiList: [],
|
||||
// children: []
|
||||
// });
|
||||
// }
|
||||
// posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
// }
|
||||
|
||||
|
||||
// // Step 2: Buat struktur pohon berdasarkan parentId
|
||||
// const root: any[] = [];
|
||||
|
||||
// posisiMap.forEach((posisi) => {
|
||||
// if (posisi.parentId) {
|
||||
// const parent = posisiMap.get(posisi.parentId);
|
||||
// if (parent) {
|
||||
// parent.children.push(posisi);
|
||||
// }
|
||||
// } else {
|
||||
// root.push(posisi);
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Step 3: Ubah struktur ke format OrganizationChart
|
||||
// function toOrgChartFormat(node: any): any {
|
||||
// return {
|
||||
// expanded: true,
|
||||
// type: 'person',
|
||||
// styleClass: 'p-person',
|
||||
// data: {
|
||||
// name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
|
||||
// status: node.nama,
|
||||
// image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
|
||||
// },
|
||||
// children: node.children.map(toOrgChartFormat)
|
||||
// };
|
||||
// }
|
||||
|
||||
|
||||
// const chartData = root.map(toOrgChartFormat);
|
||||
|
||||
// return (
|
||||
// <Box py={10}>
|
||||
// <Paper bg={colors.grey} p="md" style={{ overflowX: 'auto' }}>
|
||||
// <OrganizationChart style={{ color: colors['blue-button'] }} value={chartData} nodeTemplate={nodeTemplate} />
|
||||
// </Paper>
|
||||
// </Box>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
// function nodeTemplate(node: any) {
|
||||
// const imageSrc = node?.data?.image || '/img/default.png';
|
||||
// const name = node?.data?.name || 'Tanpa Nama';
|
||||
// const status = node?.data?.status || 'Tidak ada deskripsi';
|
||||
|
||||
// return (
|
||||
// <Stack pos={"relative"} py={"xl"} gap={"22"}>
|
||||
// <Stack align="center" gap={4}>
|
||||
// <Image
|
||||
// src={imageSrc}
|
||||
// alt={name}
|
||||
// radius="xl"
|
||||
// w={120}
|
||||
// h={120}
|
||||
// fit="cover"
|
||||
// />
|
||||
// <Text fw={600} ta="center">{name}</Text>
|
||||
// <Text size="sm" c="dimmed" ta="center">{status}</Text>
|
||||
// </Stack>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default Page;
|
||||
|
||||
'use client'
|
||||
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -136,16 +16,27 @@ import {
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
Transition,
|
||||
} from '@mantine/core'
|
||||
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconUsers,
|
||||
IconZoomIn,
|
||||
IconZoomOut,
|
||||
IconArrowsMaximize,
|
||||
IconArrowsMinimize,
|
||||
} from '@tabler/icons-react'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
import colors from '@/con/colors'
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -167,7 +58,6 @@ export default function Page() {
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
|
||||
>
|
||||
Struktur Organisasi PPID
|
||||
</Title>
|
||||
@@ -180,20 +70,34 @@ export default function Page() {
|
||||
<StrukturOrganisasiPPID />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function StrukturOrganisasiPPID() {
|
||||
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
||||
const router = useTransitionRouter()
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// debounce untuk pencarian
|
||||
const debouncedSearch = useRef(
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value)
|
||||
}, 400)
|
||||
).current
|
||||
|
||||
useEffect(() => {
|
||||
void stateOrganisasi.findMany.load()
|
||||
}, [])
|
||||
|
||||
const isLoading =
|
||||
!stateOrganisasi.findMany.data &&
|
||||
stateOrganisasi.findMany.loading !== false
|
||||
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -209,10 +113,7 @@ function StrukturOrganisasiPPID() {
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!stateOrganisasi.findMany.data ||
|
||||
stateOrganisasi.findMany.data.length === 0
|
||||
) {
|
||||
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||
return (
|
||||
<Center py={40}>
|
||||
<Stack align="center" gap="md">
|
||||
@@ -233,8 +134,7 @@ function StrukturOrganisasiPPID() {
|
||||
Data pegawai belum tersedia
|
||||
</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
Belum ada data pegawai yang tercatat untuk PPID. Silakan coba
|
||||
muat ulang atau periksa sumber data.
|
||||
Belum ada data pegawai yang tercatat untuk PPID.
|
||||
</Text>
|
||||
<Group justify="center" mt="lg">
|
||||
<Button
|
||||
@@ -245,15 +145,6 @@ function StrukturOrganisasiPPID() {
|
||||
>
|
||||
Muat Ulang
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconSearch size={16} />}
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
stateOrganisasi.findMany.load({ query: { q: '' } })
|
||||
}
|
||||
>
|
||||
Cari Pegawai
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -261,45 +152,42 @@ function StrukturOrganisasiPPID() {
|
||||
)
|
||||
}
|
||||
|
||||
// Buat struktur organisasi
|
||||
const posisiMap = new Map<string, any>()
|
||||
|
||||
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
|
||||
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive)
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id;
|
||||
const posisiId = pegawai.posisi.id
|
||||
if (!posisiMap.has(posisiId)) {
|
||||
posisiMap.set(posisiId, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: [],
|
||||
});
|
||||
})
|
||||
}
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
|
||||
}
|
||||
|
||||
const root: any[] = []
|
||||
posisiMap.forEach((posisi) => {
|
||||
if (posisi.parentId) {
|
||||
const parent = posisiMap.get(posisi.parentId)
|
||||
if (parent) {
|
||||
parent.children.push(posisi)
|
||||
} else {
|
||||
root.push(posisi)
|
||||
}
|
||||
} else {
|
||||
root.push(posisi)
|
||||
}
|
||||
if (parent) parent.children.push(posisi)
|
||||
else root.push(posisi)
|
||||
} else root.push(posisi)
|
||||
})
|
||||
|
||||
function toOrgChartFormat(node: any): any {
|
||||
const pegawai = node.pegawaiList?.[0]
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
|
||||
id: pegawai?.id || null,
|
||||
name: pegawai?.namaLengkap || 'Belum ditugaskan',
|
||||
title: node.nama || 'Tanpa jabatan',
|
||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
|
||||
image: pegawai?.image?.link || '/img/default.png',
|
||||
description: node.deskripsi || '',
|
||||
positionId: node.id || null,
|
||||
},
|
||||
@@ -307,36 +195,120 @@ function StrukturOrganisasiPPID() {
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = root.map(toOrgChartFormat)
|
||||
let chartData = root.map(toOrgChartFormat)
|
||||
|
||||
// 🔍 filter by search
|
||||
if (searchQuery) {
|
||||
const filterNodes = (nodes: any[]): any[] =>
|
||||
nodes
|
||||
.map((n) => ({
|
||||
...n,
|
||||
children: filterNodes(n.children || []),
|
||||
}))
|
||||
.filter(
|
||||
(n) =>
|
||||
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
n.children.length > 0
|
||||
)
|
||||
chartData = filterNodes(chartData)
|
||||
}
|
||||
|
||||
// 🧭 fungsi fullscreen
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
chartContainerRef.current?.requestFullscreen()
|
||||
setFullscreen(true)
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
setFullscreen(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 🧭 fungsi zoom
|
||||
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
|
||||
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
|
||||
const resetZoom = () => setScale(1)
|
||||
|
||||
return (
|
||||
<Box py={16} >
|
||||
<Paper
|
||||
radius="md"
|
||||
p="md"
|
||||
<Stack align="center" mt="xl">
|
||||
{/* 🔍 Search + Zoom + Fullscreen controls */}
|
||||
<Group mb="md" justify="center" gap="sm" align="center">
|
||||
<TextInput
|
||||
placeholder="Cari nama atau jabatan..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onChange={(e) => debouncedSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button variant="light" size="sm" onClick={handleZoomOut}>
|
||||
<IconZoomOut size={16} />
|
||||
</Button>
|
||||
|
||||
{/* 🔍 Tambahkan indikator zoom di sini */}
|
||||
{/* Floating Zoom Indicator */}
|
||||
<Box
|
||||
bg="#C3D0E8"
|
||||
c="blue"
|
||||
px={9}
|
||||
py={8}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
|
||||
|
||||
<Button variant="light" size="sm" onClick={handleZoomIn}>
|
||||
<IconZoomIn size={16} />
|
||||
</Button>
|
||||
|
||||
<Button variant="light" size="sm" onClick={resetZoom}>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />
|
||||
}
|
||||
>
|
||||
{isFullscreen ? 'Keluar' : 'Fullscreen'}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
|
||||
{/* Chart Container */}
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
background: 'rgba(28,110,164,0.2)',
|
||||
border: `1px solid rgba(255,255,255,0.1)`,
|
||||
overflowX: 'auto',
|
||||
overflow: 'auto',
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
transition: 'transform 0.25s ease',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeTemplate={(node) => nodeTemplate(node, router)}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function nodeTemplate(node: any) {
|
||||
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const description = node?.data?.description || ''
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
radius="lg"
|
||||
@@ -357,15 +329,15 @@ function nodeTemplate(node: any) {
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
radius="md"
|
||||
width={120}
|
||||
height={120}
|
||||
width={60}
|
||||
height={60}
|
||||
fit="cover"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
border: '2px solid rgba(255,255,255,0.2)',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
loading='lazy'
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text fw={700}>{name}</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
@@ -374,19 +346,17 @@ function nodeTemplate(node: any) {
|
||||
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
|
||||
<Tooltip label="Lihat Detail" withArrow position="bottom">
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
mt="md"
|
||||
onClick={() => {
|
||||
const id = node?.data?.positionId
|
||||
if (id && (window as any).scrollTo) {
|
||||
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
const id = node?.data?.id
|
||||
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
|
||||
}}
|
||||
>
|
||||
Kembali
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
@@ -394,6 +364,3 @@ function nodeTemplate(node: any) {
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -54,12 +54,11 @@ function Page() {
|
||||
ta="center"
|
||||
fz={{ base: 28, md: 36 }}
|
||||
fw={800}
|
||||
variant="gradient"
|
||||
gradient={{ from: colors['blue-button'], to: 'cyan', deg: 45 }}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Moto PPID Desa Darmasaba
|
||||
</Text>
|
||||
<Text ta="center" fz={{ base: 16, md: 20 }} mt="xs" c="dimmed">
|
||||
<Text ta="center" fz={{ base: 16, md: 20 }} mt="xs">
|
||||
Memberikan informasi yang cepat, mudah, tepat, dan transparan
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -73,7 +72,7 @@ function Page() {
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
ta="center"
|
||||
dangerouslySetInnerHTML={{ __html: item.visi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
@@ -88,7 +87,6 @@ function Page() {
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
dangerouslySetInnerHTML={{ __html: item.misi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
'use client';
|
||||
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
|
||||
import colors from "@/con/colors";
|
||||
import { Carousel, CarouselSlide } from "@mantine/carousel";
|
||||
import { Box, Button, Container, Group, Paper, Stack, Text, useMantineTheme, Skeleton } from "@mantine/core";
|
||||
import { Carousel } from "@mantine/carousel";
|
||||
import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text, useMantineTheme } from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { IconArrowRight, IconAward } from "@tabler/icons-react";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import { IconAward, IconArrowRight } from "@tabler/icons-react";
|
||||
import { useTransitionRouter } from "next-view-transitions";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
@@ -18,7 +18,8 @@ export default function Page() {
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "60%" }}>
|
||||
<Container w={{ base: "100%", md: "90%", lg: "60%" }}>
|
||||
|
||||
<Stack align="center" gap="sm">
|
||||
<Group gap="xs">
|
||||
<IconAward size={40} color={colors["blue-button"]} />
|
||||
@@ -37,11 +38,10 @@ export default function Page() {
|
||||
}
|
||||
|
||||
function Slider() {
|
||||
const height = 500;
|
||||
const width = 1200;
|
||||
const theme = useMantineTheme();
|
||||
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
|
||||
const autoplay = useRef(Autoplay({ delay: 3000 }));
|
||||
const tablet = useMediaQuery(`(max-width: ${theme.breakpoints.md})`);
|
||||
const autoplay = useRef(Autoplay({ delay: 3000, stopOnInteraction: false }));
|
||||
const state = useProxy(penghargaanState);
|
||||
const router = useTransitionRouter();
|
||||
|
||||
@@ -54,7 +54,7 @@ function Slider() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Group justify="center" py="xl">
|
||||
<Group justify="center" py="xl" gap="md">
|
||||
<Skeleton w={300} h={200} radius="lg" />
|
||||
<Skeleton w={300} h={200} radius="lg" visibleFrom="sm" />
|
||||
<Skeleton w={300} h={200} radius="lg" visibleFrom="md" />
|
||||
@@ -74,31 +74,49 @@ function Slider() {
|
||||
}
|
||||
|
||||
const slides = data.map((item) => (
|
||||
<CarouselSlide key={item.id}>
|
||||
<Carousel.Slide key={item.id}>
|
||||
<Paper
|
||||
h="100%"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
pos="relative"
|
||||
style={{
|
||||
height: "100%",
|
||||
backgroundImage: `url(${item.image?.link})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
transition: "transform 0.3s ease, box-shadow 0.3s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(-4px)";
|
||||
e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.2)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
pos="absolute"
|
||||
inset={0}
|
||||
bg="linear-gradient(to top, rgba(0,0,0,0.7), rgba(0,0,0,0.3))"
|
||||
bg="linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0.2))"
|
||||
style={{ borderRadius: 16 }}
|
||||
/>
|
||||
<Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative">
|
||||
<Text fz="xl" fw={700} ta="center" c="white">
|
||||
<Text
|
||||
fz={{ base: "md", sm: "lg", md: "xl" }}
|
||||
fw={700}
|
||||
ta="center"
|
||||
c="white"
|
||||
lineClamp={3}
|
||||
style={{ textShadow: "0 2px 4px rgba(0,0,0,0.6)" }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button
|
||||
onClick={() => router.push(`/darmasaba/penghargaan/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/penghargaan/${item.id}`)
|
||||
}
|
||||
size="md"
|
||||
radius="xl"
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
@@ -110,24 +128,83 @@ function Slider() {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</CarouselSlide>
|
||||
</Carousel.Slide>
|
||||
));
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
py="xl"
|
||||
plugins={[autoplay.current]}
|
||||
onMouseEnter={autoplay.current.stop}
|
||||
onMouseLeave={autoplay.current.reset}
|
||||
w={{ base: "100%", sm: "90%", md: "80%", lg: width }}
|
||||
h={height}
|
||||
slideSize={{ base: "100%", sm: "50%", md: "33.333333%" }}
|
||||
slideGap="md"
|
||||
loop
|
||||
align="start"
|
||||
slidesToScroll={mobile ? 1 : 2}
|
||||
<Box
|
||||
pos="relative"
|
||||
w="100%"
|
||||
mx="auto"
|
||||
px={{ base: "md", sm: "xl", md: "2rem", lg: "3rem" }}
|
||||
style={{
|
||||
maxWidth: 1300,
|
||||
}}
|
||||
>
|
||||
{slides}
|
||||
</Carousel>
|
||||
<Carousel
|
||||
py="xl"
|
||||
w="100%"
|
||||
h={{ base: 320, sm: 380, md: 420, lg: 450 }}
|
||||
slideSize={{
|
||||
base: "100%", // Mobile: 1
|
||||
sm: "50%", // Tablet kecil (≥768): 2
|
||||
md: "50%", // 1024px: tetap 2
|
||||
lg: "33.333%", // Desktop besar: 3
|
||||
}}
|
||||
slideGap={{ base: "md", sm: "md", md: "lg" }}
|
||||
loop
|
||||
align="start"
|
||||
slidesToScroll={mobile ? 1 : tablet ? 2 : 3}
|
||||
plugins={[autoplay.current]}
|
||||
onMouseEnter={autoplay.current.stop}
|
||||
onMouseLeave={autoplay.current.reset}
|
||||
withControls={data.length > 3}
|
||||
draggable={data.length > 1}
|
||||
styles={{
|
||||
root: {
|
||||
position: "relative",
|
||||
},
|
||||
viewport: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
container: {
|
||||
alignItems: "stretch",
|
||||
},
|
||||
control: {
|
||||
zIndex: 20,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
color: colors["blue-button"],
|
||||
border: `2px solid ${colors["blue-button"]}`,
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: "50%",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
transition: "all 0.2s ease",
|
||||
'&:hover': {
|
||||
backgroundColor: colors["blue-button"],
|
||||
color: "white",
|
||||
transform: "scale(1.1)",
|
||||
},
|
||||
'&[data-inactive]': {
|
||||
opacity: 0,
|
||||
cursor: 'default',
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
position: "absolute",
|
||||
top: mobile ? "70%" : tablet ? "65%" : "60%",
|
||||
transform: "translateY(-50%)",
|
||||
width: mobile ? "100%" : tablet ? "calc(100% + 60px)" : "calc(100% + 100px)",
|
||||
left: mobile ? "0" : tablet ? "-30px" : "-50px",
|
||||
right: mobile ? "0" : tablet ? "-30px" : "-50px",
|
||||
padding: "0",
|
||||
justifyContent: "space-between",
|
||||
zIndex: 30,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{slides}
|
||||
</Carousel>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,100 @@
|
||||
'use client'
|
||||
import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
|
||||
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTiktok, IconBrandYoutube } from '@tabler/icons-react';
|
||||
import { useRef } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const sosialMedia = [
|
||||
{
|
||||
title: "Facebook",
|
||||
link: "https://www.facebook.com/DarmasabaDesaku",
|
||||
icon: IconBrandFacebook,
|
||||
},
|
||||
{
|
||||
title: "Instagram",
|
||||
link: "https://www.instagram.com/ddarmasaba/",
|
||||
icon: IconBrandInstagram,
|
||||
},
|
||||
{
|
||||
title: "Youtube",
|
||||
link: "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
|
||||
icon: IconBrandYoutube,
|
||||
},
|
||||
{
|
||||
title: "Tiktok",
|
||||
link: "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
|
||||
icon: IconBrandTiktok,
|
||||
},
|
||||
]
|
||||
|
||||
const layanandesa = [
|
||||
{
|
||||
title: "Administrasi Kependudukan",
|
||||
link: "/darmasaba/desa/layanan/",
|
||||
},
|
||||
{
|
||||
title: "Layanan Sosial",
|
||||
link: "/darmasaba/ekonomi/program-kemiskinan",
|
||||
},
|
||||
{
|
||||
title: "Pengaduan Masyarakat",
|
||||
link: "/darmasaba/keamanan/laporan-publik",
|
||||
},
|
||||
{
|
||||
title: "Informasi Publik",
|
||||
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
|
||||
},
|
||||
]
|
||||
|
||||
const tautanPenting = [
|
||||
{
|
||||
title: "Portal Badung",
|
||||
link: "/darmasaba/desa/berita/semua",
|
||||
},
|
||||
{
|
||||
title: "E-Government",
|
||||
link: "/darmasaba/inovasi/desa-digital-smart-village",
|
||||
},
|
||||
{
|
||||
title: "Transparansi",
|
||||
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
|
||||
}
|
||||
]
|
||||
|
||||
function Footer() {
|
||||
|
||||
const emailRef = useRef<HTMLInputElement>(null)
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = emailRef.current?.value.trim();
|
||||
if (!email) return toast.error('Email wajib diisi!');
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) return toast.error('Format email tidak valid!');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.success) {
|
||||
toast.success('Berhasil! Cek email Anda untuk konfirmasi.');
|
||||
emailRef.current!.value = '';
|
||||
} else {
|
||||
toast.error(data.message || 'Gagal berlangganan.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Gagal menghubungi server. Coba lagi nanti.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white">
|
||||
<Box w="100%" p="xl">
|
||||
@@ -46,7 +138,7 @@ function Footer() {
|
||||
<Group justify="apart" align="center" mt="lg">
|
||||
<Text c="#F3F2EC" ta="center" fz="md" fw={700} style={{ fontStyle: 'italic' }}>"Desa Kuat, Warga Sejahtera!"</Text>
|
||||
<ActionIcon size={80} radius="xl" variant="transparent">
|
||||
<Image src="/chatbot-removebg-preview.png" alt="Logo Desa" width={80} height={80} loading="lazy"/>
|
||||
<Image src="/chatbot-removebg-preview.png" alt="Logo Desa" width={80} height={80} loading="lazy" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -64,31 +156,39 @@ function Footer() {
|
||||
Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||
</Text>
|
||||
<Flex gap="md" mt="sm" c="#F3F2EC">
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandFacebook size={22} /></ActionIcon>
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandInstagram size={22} /></ActionIcon>
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandTwitter size={22} /></ActionIcon>
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandWhatsapp size={22} /></ActionIcon>
|
||||
{sosialMedia.map((item) => (
|
||||
<ActionIcon
|
||||
key={item.title}
|
||||
component="a"
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="subtle"
|
||||
color="white"
|
||||
>
|
||||
<item.icon size={22} />
|
||||
</ActionIcon>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Text c="white" fz="md" fw={700}>Layanan Desa</Text>
|
||||
<Anchor c="#F3F2EC" fz="xs">Administrasi Kependudukan</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Layanan Sosial</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Pengaduan Masyarakat</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Informasi Publik</Anchor>
|
||||
{layanandesa.map((item) => (
|
||||
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Text c="white" fz="md" fw={700}>Tautan Penting</Text>
|
||||
<Anchor c="#F3F2EC" fz="xs">Portal Badung</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">E-Government</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Transparansi</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Unduhan</Anchor>
|
||||
{tautanPenting.map((item) => (
|
||||
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -101,8 +201,9 @@ function Footer() {
|
||||
w="70%"
|
||||
placeholder="Masukkan email Anda"
|
||||
rightSection={<IconAt size={16} />}
|
||||
ref={emailRef} // ini aja cukup
|
||||
/>
|
||||
<Button variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} radius="md">Daftar</Button>
|
||||
<Button onClick={handleSubmit} variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} radius="md">Daftar</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -1,20 +1,77 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import stateNav from "@/state/state-nav";
|
||||
import { Container, Stack, Tooltip } from "@mantine/core";
|
||||
import { Container, Stack, ActionIcon, Box } from "@mantine/core";
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import GlobalSearch from "./globalSearch";
|
||||
|
||||
export function NavbarSearch() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isNavigatingRef = useRef(false);
|
||||
|
||||
// Close when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Jangan close jika klik di search result item (biar handleSelect yang urus)
|
||||
if (target.closest('.search-result-item')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close jika klik di luar container
|
||||
if (containerRef.current && !containerRef.current.contains(target)) {
|
||||
setIsOpen(false);
|
||||
stateNav.clear();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Reset navigation flag saat component unmount atau route change
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isNavigatingRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container
|
||||
w={{ base: "100%", md: "80%" }}
|
||||
fluid
|
||||
py="xl"
|
||||
onMouseLeave={stateNav.clear}
|
||||
<Box
|
||||
ref={containerRef}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<Stack pt="xl">
|
||||
<Tooltip label="Type to search across the site" position="bottom-start" withArrow>
|
||||
<GlobalSearch />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Container
|
||||
w={{ base: "100%", md: "80%" }}
|
||||
fluid
|
||||
py="xl"
|
||||
>
|
||||
<Stack pt="xl">
|
||||
<Box style={{ position: 'relative' }}>
|
||||
<GlobalSearch />
|
||||
{isOpen && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
stateNav.clear();
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1000
|
||||
}}
|
||||
>
|
||||
<IconX size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import stateNav from "@/state/state-nav";
|
||||
import { ActionIcon, Box, Burger, Group, Image, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { IconSquareArrowRight } from "@tabler/icons-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { MenuItem } from "../../../../types/menu-item";
|
||||
import { NavbarMainMenu } from "./NavbarMainMenu";
|
||||
@@ -19,14 +19,18 @@ export function Navbar() {
|
||||
<Paper
|
||||
radius="0"
|
||||
className="glass2"
|
||||
w="100%"
|
||||
w="100vw"
|
||||
pos="fixed"
|
||||
top={0}
|
||||
style={{ zIndex: 100 }}
|
||||
>
|
||||
<NavbarMainMenu listNavbar={navbarListMenu} />
|
||||
{/* Desktop navbar (muncul mulai 992px ke atas) */}
|
||||
<Box visibleFrom="md">
|
||||
<NavbarMainMenu listNavbar={navbarListMenu} />
|
||||
</Box>
|
||||
|
||||
<Box hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm">
|
||||
{/* Mobile navbar (muncul di bawah 992px, termasuk iPad Mini) */}
|
||||
<Box hiddenFrom="md" bg={colors.grey[2]} px="md" py="sm">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
@@ -37,11 +41,22 @@ export function Navbar() {
|
||||
stateNav.mobileOpen = false;
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Go to homepage" position="bottom" withArrow>
|
||||
<Image src="/darmasaba-icon.png" alt="Village Logo" width={48} height={48} loading="lazy"/>
|
||||
<Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
|
||||
<Image
|
||||
src="/darmasaba-icon.png"
|
||||
alt="Village Logo"
|
||||
width={48}
|
||||
height={48}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
<Tooltip label={mobileOpen ? "Close menu" : "Open menu"} position="bottom" withArrow>
|
||||
|
||||
<Tooltip
|
||||
label={mobileOpen ? "Close menu" : "Open menu"}
|
||||
position="bottom"
|
||||
withArrow
|
||||
>
|
||||
<Burger
|
||||
opened={mobileOpen}
|
||||
color={colors["blue-button"]}
|
||||
@@ -50,12 +65,14 @@ export function Navbar() {
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{mobileOpen && (
|
||||
<Paper
|
||||
component={motion.div}
|
||||
initial={{ x: '100%' }}
|
||||
bg="white"
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '100%' }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
pos="absolute"
|
||||
left={0}
|
||||
@@ -63,12 +80,14 @@ export function Navbar() {
|
||||
top="100%"
|
||||
m={0}
|
||||
radius={0}
|
||||
shadow="md"
|
||||
>
|
||||
<NavbarMobile listNavbar={navbarListMenu} />
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{(item || isSearch) && <Box className="glass" />}
|
||||
</Box>
|
||||
);
|
||||
@@ -76,35 +95,105 @@ export function Navbar() {
|
||||
|
||||
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname(); // 👈 untuk cek path aktif
|
||||
|
||||
// fungsi bantu: cek apakah path sekarang sama dengan menu / sub-menu
|
||||
const isActive = (href?: string) => href && pathname.startsWith(href);
|
||||
|
||||
return (
|
||||
<ScrollArea.Autosize mah="calc(100vh - 80px)" offsetScrollbars>
|
||||
<Stack p="md" gap="xs">
|
||||
{listNavbar.map((item, k) => (
|
||||
<Box key={k}>
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="xs"
|
||||
onClick={() => {
|
||||
router.push(item.href);
|
||||
stateNav.mobileOpen = false;
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Text c="dark.9" fw={600} fz="md">
|
||||
{item.name}
|
||||
</Text>
|
||||
<IconSquareArrowRight size={18} />
|
||||
</Group>
|
||||
{item.children && (
|
||||
<Box pl="md">
|
||||
<NavbarMobile listNavbar={item.children} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<ScrollArea.Autosize
|
||||
mah="calc(100dvh - 80px)"
|
||||
type="auto"
|
||||
offsetScrollbars
|
||||
>
|
||||
<Stack p="sm" gap="xs">
|
||||
{listNavbar.map((item, k) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<Box key={k}>
|
||||
<Paper
|
||||
shadow={active ? "sm" : "xs"}
|
||||
radius="md"
|
||||
p="sm"
|
||||
withBorder
|
||||
bg={active ? "blue.0" : "gray.0"}
|
||||
onClick={() => {
|
||||
if (item.href) {
|
||||
router.push(item.href);
|
||||
stateNav.mobileOpen = false;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: item.href ? "pointer" : "default",
|
||||
transition: "background 0.15s ease",
|
||||
borderLeft: active ? "4px solid #1e66f5" : "4px solid transparent",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Text
|
||||
fw={active ? 700 : 600}
|
||||
fz="md"
|
||||
c={active ? "blue.7" : "dark.9"}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.href && (
|
||||
<IconSquareArrowRight
|
||||
size={18}
|
||||
color={active ? "#1e66f5" : "inherit"}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* Submenu */}
|
||||
{item.children && (
|
||||
<Box pl="md" mt={4}>
|
||||
{item.children.map((child, j) => {
|
||||
const childActive = isActive(child.href);
|
||||
return (
|
||||
<Group
|
||||
key={j}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p="xs"
|
||||
onClick={() => {
|
||||
if (child.href) {
|
||||
router.push(child.href);
|
||||
stateNav.mobileOpen = false;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: child.href ? "pointer" : "default",
|
||||
opacity: child.href ? 1 : 0.8,
|
||||
borderRadius: "0.5rem",
|
||||
backgroundColor: childActive ? "#e7f0ff" : "transparent",
|
||||
borderLeft: childActive ? "3px solid #1e66f5" : "3px solid transparent",
|
||||
transition: "background 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={childActive ? 600 : 400}
|
||||
c={childActive ? "blue.7" : "dark.8"}
|
||||
>
|
||||
{child.name}
|
||||
</Text>
|
||||
<IconSquareArrowRight
|
||||
size={14}
|
||||
color={childActive ? "#1e66f5" : "inherit"}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</ScrollArea.Autosize>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import colors from "@/con/colors"
|
||||
import stateNav from "@/state/state-nav"
|
||||
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core"
|
||||
import { useHover } from "@mantine/hooks"
|
||||
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
|
||||
import { IconSearch, IconUser } from "@tabler/icons-react"
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { MenuItem } from "../../../../types/menu-item"
|
||||
import { NavbarSearch } from "./NavBarSearch"
|
||||
import { NavbarSubMenu } from "./NavbarSubMenu"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
|
||||
const stateAuth = {
|
||||
@@ -21,12 +20,13 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
const { item, isSearch } = useSnapshot(stateNav)
|
||||
const router = useTransitionRouter()
|
||||
const next = useRouter()
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
|
||||
<Container pos="relative" w={{ base: '100%', md: '80%' }} fluid>
|
||||
<Flex align="center" justify="space-between" wrap={{ base: "wrap", md: "nowrap" }}>
|
||||
<Tooltip label="Go to Homepage" position="bottom" withArrow>
|
||||
<Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
radius="xl"
|
||||
variant="transparent"
|
||||
@@ -47,10 +47,15 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
</Tooltip>
|
||||
|
||||
{listNavbar.map((item, k) => (
|
||||
<MenuItemCom key={k} item={item} />
|
||||
<MenuItemCom
|
||||
key={k}
|
||||
item={item}
|
||||
isActive={item.href && pathname.startsWith(item.href) ||
|
||||
(item.children?.some(child => child.href && pathname.startsWith(child.href)))}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Tooltip label="Search content" position="bottom" withArrow>
|
||||
<Tooltip label="Cari Konten" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
c={isSearch ? 'gray' : colors["blue-button"]}
|
||||
@@ -66,7 +71,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
|
||||
{/* hanya tampil kalau role = admin */}
|
||||
{stateAuth.role === "admin" && (
|
||||
<Tooltip label="My Profile" position="bottom" withArrow>
|
||||
<Tooltip label="Profil Saya" position="bottom" withArrow>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
next.push("/admin/landing-page/profile/program-inovasi")
|
||||
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MenuItemCom({ item }: { item: MenuItem }) {
|
||||
const { ref, hovered } = useHover()
|
||||
function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
|
||||
const router = useTransitionRouter()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
color={hovered ? "gray" : colors["blue-button"]}
|
||||
onMouseEnter={() => {
|
||||
stateNav.item = item.children || null
|
||||
stateNav.isSearch = false
|
||||
<Menu
|
||||
trigger="hover"
|
||||
position="bottom-start"
|
||||
offset={20}
|
||||
width={300}
|
||||
shadow="md"
|
||||
withinPortal
|
||||
onOpen={() => {
|
||||
stateNav.item = item.children || null;
|
||||
stateNav.isSearch = false;
|
||||
}}
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
router.push(item.href)
|
||||
stateNav.clear()
|
||||
}}
|
||||
fw={500}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
<MenuTarget>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color={isActive ? 'blue' : 'gray'}
|
||||
onClick={() => {
|
||||
if (item.href) {
|
||||
router.push(item.href);
|
||||
stateNav.clear();
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
borderBottom: isActive ? `2px solid ${colors['blue-button']}` : 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
</MenuTarget>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react";
|
||||
import { MenuItem } from "../../../../types/menu-item";
|
||||
import { useTransitionRouter } from "next-view-transitions";
|
||||
import colors from "@/con/colors";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
||||
const router = useTransitionRouter();
|
||||
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<motion.div
|
||||
key={Math.random().toString(36).slice(2)}
|
||||
@@ -32,33 +33,34 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
||||
<Stack gap="xs" align="stretch">
|
||||
{item.map((link, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="subtle"
|
||||
justify="space-between"
|
||||
size="lg"
|
||||
radius="md"
|
||||
color="gray.0"
|
||||
onClick={() => {
|
||||
key={index}
|
||||
variant="subtle"
|
||||
justify="space-between"
|
||||
size="lg"
|
||||
radius="md"
|
||||
color={link.href && pathname.startsWith(link.href) ? 'blue' : 'gray'}
|
||||
onClick={() => {
|
||||
if (link.href) {
|
||||
router.push(link.href);
|
||||
stateNav.item = null;
|
||||
stateNav.isSearch = false;
|
||||
}}
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
styles={(theme) => ({
|
||||
root: {
|
||||
background: "transparent",
|
||||
color: colors['blue-button'],
|
||||
fontWeight: 500,
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": {
|
||||
background: theme.colors.gray[8],
|
||||
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
{link.name}
|
||||
</Button>
|
||||
}
|
||||
}}
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
styles={(theme) => ({
|
||||
root: {
|
||||
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent',
|
||||
color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'],
|
||||
fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500,
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": {
|
||||
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[1] : theme.colors.gray[0],
|
||||
}
|
||||
},
|
||||
})}
|
||||
>
|
||||
{link.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
|
||||
@@ -1,97 +1,184 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
import { TextInput, Loader, Stack, Box, Text } from '@mantine/core';
|
||||
|
||||
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
|
||||
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import searchState from '@/app/api/[[...slugs]]/_lib/search/searchState';
|
||||
|
||||
// Mapping type ke URL
|
||||
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
||||
const { type, id, kategori } = item;
|
||||
|
||||
const typeUrlMap: Record<string, string> = {
|
||||
programinovasi: `/darmasaba/program-inovasi/${id}`,
|
||||
desaantikorupsi: '/darmasaba/desa-anti-korupsi',
|
||||
sdgsdesa: '/darmasaba/sdgs-desa',
|
||||
apbdes: '/darmasaba/apbdes',
|
||||
prestasidesa: '/darmasaba/prestasi-desa',
|
||||
pejabatdesa: '/darmasaba/profile/pejabat-desa',
|
||||
strukturppid: '/darmasaba/ppid/struktur-ppid',
|
||||
visimisippid: '/darmasaba/ppid/visi-misi',
|
||||
dasarhukumppid: '/darmasaba/ppid/dasar-hukum',
|
||||
profileppid: '/darmasaba/ppid/profile',
|
||||
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik',
|
||||
perbekeldarmasaba: '/darmasaba/desa/profile',
|
||||
berita: `/darmasaba/desa/berita/${kategori}/${id}`,
|
||||
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`,
|
||||
sejarahdesa: '/darmasaba/desa/profile',
|
||||
visimisidesa: '/darmasaba/desa/profile',
|
||||
lambangdesa: '/darmasaba/desa/profile',
|
||||
maskotdesa: '/darmasaba/desa/profile',
|
||||
profilperbekel: '/darmasaba/desa/profile',
|
||||
potensi: '/darmasaba/desa/potensi-desa',
|
||||
galleryFoto: '/darmasaba/desa/gallery/foto',
|
||||
galleryVideo: '/darmasaba/desa/gallery/video',
|
||||
pelayananSuratKeterangan: '/darmasaba/desa/layanan',
|
||||
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan',
|
||||
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan',
|
||||
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan',
|
||||
penghargaan: '/darmasaba/desa/penghargaan',
|
||||
};
|
||||
|
||||
return type ? typeUrlMap[type] || '/darmasaba' : '/darmasaba';
|
||||
};
|
||||
|
||||
import getDetailUrl from './searchUrl';
|
||||
|
||||
export default function GlobalSearch() {
|
||||
const snap = useSnapshot(searchState);
|
||||
const router = useRouter();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
// Infinite scroll listener
|
||||
// Buka popover saat ada query
|
||||
useEffect(() => {
|
||||
setOpened(!!snap.query);
|
||||
}, [snap.query]);
|
||||
|
||||
// Infinite scroll handler
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const bottom =
|
||||
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
|
||||
if (bottom && !snap.loading) searchState.next();
|
||||
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
|
||||
if (nearBottom && !snap.loading) searchState.next();
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [snap.loading]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<TextInput
|
||||
placeholder="Cari apapun..."
|
||||
value={snap.query}
|
||||
onChange={(e) => (searchState.query = e.currentTarget.value)}
|
||||
/>
|
||||
const handleSelect = async (e: React.MouseEvent, item: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isNavigating) return;
|
||||
setIsNavigating(true);
|
||||
|
||||
try {
|
||||
// 🔥 pastikan objek udah “dikeluarkan” dari Proxy valtio
|
||||
const rawItem = JSON.parse(JSON.stringify(item));
|
||||
|
||||
// 🔥 pastikan type-nya string murni
|
||||
const type = String(rawItem.type || '').trim().toLowerCase();
|
||||
|
||||
// 🔥 panggil getDetailUrl pakai type yang fix
|
||||
let url = getDetailUrl({ ...rawItem, type });
|
||||
|
||||
// kalau hasil undefined atau default, fallback ke link eksternal
|
||||
if (!url || url === '/darmasaba') {
|
||||
if (rawItem.link && rawItem.link.startsWith('http')) {
|
||||
url = rawItem.link;
|
||||
}
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
console.warn('URL tidak ditemukan untuk item:', rawItem);
|
||||
setIsNavigating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Navigating to:', url);
|
||||
|
||||
// tutup popover dulu
|
||||
setOpened(false);
|
||||
searchState.query = '';
|
||||
searchState.results = [];
|
||||
searchState.loading = false;
|
||||
|
||||
// kasih delay biar UI nutup dulu
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// navigasi
|
||||
if (url.startsWith('http')) {
|
||||
window.location.href = url;
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error saat navigasi:', err);
|
||||
setIsNavigating(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
{snap.results.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
p="sm"
|
||||
const clearSearch = () => {
|
||||
searchState.query = '';
|
||||
searchState.results = [];
|
||||
searchState.page = 1;
|
||||
searchState.nextPage = null;
|
||||
setOpened(false);
|
||||
setIsNavigating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
<Popover
|
||||
opened={opened && !!snap.query}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) clearSearch();
|
||||
setOpened(isOpen);
|
||||
}}
|
||||
width="target"
|
||||
position="bottom"
|
||||
shadow="md"
|
||||
withinPortal
|
||||
radius="md"
|
||||
zIndex={2000}
|
||||
closeOnClickOutside={true}
|
||||
closeOnEscape={true}
|
||||
styles={{
|
||||
dropdown: {
|
||||
zIndex: 2000,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<TextInput
|
||||
placeholder="Cari apapun..."
|
||||
value={snap.query}
|
||||
onChange={(e) => {
|
||||
searchState.query = e.currentTarget.value;
|
||||
debouncedFetch();
|
||||
}}
|
||||
radius="xl"
|
||||
size="md"
|
||||
rightSection={
|
||||
snap.query ? (
|
||||
<IconX
|
||||
size={16}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={clearSearch}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown
|
||||
p={0}
|
||||
style={{
|
||||
borderBottom: '1px solid #eee',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
onClick={() => {
|
||||
const url = getDetailUrl(item);
|
||||
router.push(url);
|
||||
maxHeight: 350,
|
||||
overflowY: 'auto',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #eee',
|
||||
}}
|
||||
>
|
||||
<Text size="sm" fw={500}>
|
||||
{item.judul || item.namaPasar || item.nama || item.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
dari modul: {item.type}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{snap.loading && <Loader size="sm" />}
|
||||
</Stack>
|
||||
{[...snap.results].length > 0 ? (
|
||||
[...snap.results].map((item: any, i: number) => (
|
||||
<Box
|
||||
key={i}
|
||||
p="sm"
|
||||
className="search-result-item" // Add class untuk prevent close
|
||||
style={{
|
||||
borderBottom: '1px solid #f1f1f1',
|
||||
cursor: isNavigating ? 'wait' : 'pointer',
|
||||
background: 'white',
|
||||
transition: 'background 0.2s',
|
||||
opacity: isNavigating ? 0.6 : 1,
|
||||
}}
|
||||
onMouseEnter={(e) => !isNavigating && (e.currentTarget.style.background = '#f9f9f9')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'white')}
|
||||
onClick={(e) => handleSelect(e, item)}
|
||||
>
|
||||
<Text size="sm" fw={500} lineClamp={1}>
|
||||
{item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
dari modul: {item.type || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Center py="md">
|
||||
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
|
||||
</Center>
|
||||
)}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -34,13 +34,13 @@ function Apbdes() {
|
||||
const data = (state.findMany.data || []).slice(0, 3)
|
||||
|
||||
return (
|
||||
<Stack p="lg" gap="4rem" bg={colors.Bg}>
|
||||
<Stack p="sm" gap="xl" bg={colors.Bg}>
|
||||
<Box>
|
||||
<Stack gap="sm">
|
||||
<Text ta={"center"} fz={{ base: '2.4rem', sm: '4rem' }} fw="bold" lh={1.2}>
|
||||
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
|
||||
{textHeading.title}
|
||||
</Text>
|
||||
<Text ta={"center"} fz={{ base: '1rem', sm: '1.3rem' }} c="dimmed">
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||
{textHeading.des}
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -117,7 +117,7 @@ function Apbdes() {
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
<Group pb={80} justify="center">
|
||||
<Group justify="center" pb={10}>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/darmasaba/apbdes"
|
||||
|
||||
@@ -30,9 +30,9 @@ function DesaAntiKorupsi() {
|
||||
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
|
||||
return (
|
||||
<Stack gap={"0"} bg={colors.Bg} p={"sm"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"} >
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
|
||||
<Center>
|
||||
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
|
||||
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
|
||||
</Center>
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
|
||||
<Center py={20}>
|
||||
|
||||
@@ -23,7 +23,7 @@ function Kepuasan() {
|
||||
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
||||
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
||||
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
|
||||
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
|
||||
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -121,18 +121,18 @@ function Kepuasan() {
|
||||
|
||||
// Convert map to array and sort by date
|
||||
const barData = Array.from(monthYearMap.entries())
|
||||
.map(([key, count]) => {
|
||||
.map(([key, Responden]) => {
|
||||
const [year, month] = key.split('-');
|
||||
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
||||
.toLocaleString('id-ID', { month: 'long' });
|
||||
return {
|
||||
month: `${monthName} ${year}`,
|
||||
count,
|
||||
Responden,
|
||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.sortKey - b.sortKey)
|
||||
.map(({ month, count }) => ({ month, count }));
|
||||
.map(({ month, Responden }) => ({ month, Responden }));
|
||||
|
||||
setBarChartData(barData);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ function Kepuasan() {
|
||||
|
||||
if ((loading && !data) || !data) {
|
||||
return (
|
||||
<Stack py={10} px="xl">
|
||||
<Stack py={10} px="sm">
|
||||
<Skeleton height={300} mb="md" />
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
<Skeleton height={300} />
|
||||
@@ -154,11 +154,11 @@ function Kepuasan() {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack p="sm">
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"sm"}>
|
||||
<Center>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
</Center>
|
||||
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
|
||||
<Center mt={10}>
|
||||
<Button
|
||||
radius={"lg"}
|
||||
@@ -168,7 +168,7 @@ function Kepuasan() {
|
||||
>Ajukan Responden</Button>
|
||||
</Center>
|
||||
</Container>
|
||||
<Box px={"xl"}>
|
||||
<Box px={"sm"}>
|
||||
<Paper p={"lg"} bg={colors.Bg}>
|
||||
<Paper p={"lg"}>
|
||||
<Stack gap={"xs"}>
|
||||
@@ -185,7 +185,7 @@ function Kepuasan() {
|
||||
h={window.innerWidth < 480 ? 200 : 300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
||||
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
@@ -332,7 +332,7 @@ function Kepuasan() {
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
defaultValue={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.currentTarget.value;
|
||||
@@ -416,16 +416,16 @@ function Kepuasan() {
|
||||
}
|
||||
return (
|
||||
<Stack p={"sm"}>
|
||||
<Container size="lg" px="md">
|
||||
<Container size="lg" px="sm">
|
||||
<Center>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }} c={colors["blue-button"]}>Indeks Kepuasan Masyarakat</Text>
|
||||
</Center>
|
||||
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
|
||||
<Center mt={10}>
|
||||
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||
</Center>
|
||||
</Container>
|
||||
<Box px={"xl"}>
|
||||
<Box px={"md"}>
|
||||
<Paper p={"lg"} bg={colors.Bg}>
|
||||
<Paper p={"lg"}>
|
||||
<Stack gap={"xs"}>
|
||||
@@ -448,7 +448,7 @@ function Kepuasan() {
|
||||
h={300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
||||
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
@@ -605,7 +605,7 @@ function Kepuasan() {
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
label="Tanggal Pengisian"
|
||||
type="date"
|
||||
placeholder="masukkan tanggal"
|
||||
defaultValue={state.create.form.tanggal}
|
||||
|
||||
@@ -6,20 +6,19 @@ import {
|
||||
Center,
|
||||
Image,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
Skeleton,
|
||||
useMantineColorScheme,
|
||||
ScrollArea,
|
||||
useMantineColorScheme
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { IconPhotoOff } from "@tabler/icons-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTransitionRouter } from "next-view-transitions";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { IconPhotoOff } from "@tabler/icons-react";
|
||||
|
||||
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
|
||||
|
||||
@@ -30,44 +29,42 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
||||
|
||||
return (
|
||||
<motion.div whileHover={{ scale: 1.03 }}>
|
||||
<Tooltip label={`Lihat ${data.name}`} withArrow>
|
||||
<Paper
|
||||
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer transition-all"
|
||||
bg={isDark ? "dark.6" : "white"}
|
||||
>
|
||||
<Center h={160}>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
h={140}
|
||||
w="100%"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Stack align="center" gap="xs">
|
||||
<IconPhotoOff size={38} stroke={1.5} />
|
||||
<Text size="sm" c="dimmed">
|
||||
Belum ada gambar
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Center>
|
||||
<Box mt="md">
|
||||
<Text fw={600} ta="center" size="md">
|
||||
{data.name}
|
||||
</Text>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Tooltip>
|
||||
<Paper
|
||||
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer transition-all"
|
||||
bg={isDark ? "dark.6" : "white"}
|
||||
>
|
||||
<Center h={160}>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name}
|
||||
radius="md"
|
||||
fit="contain"
|
||||
h={140}
|
||||
w="100%"
|
||||
style={{ objectPosition: "center" }}
|
||||
/>
|
||||
) : (
|
||||
<Stack align="center" gap="xs">
|
||||
<IconPhotoOff size={38} stroke={1.5} />
|
||||
<Text size="sm" c="dimmed">
|
||||
Belum ada gambar
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Center>
|
||||
<Box mt="md">
|
||||
<Text fw={600} ta="center" size="md">
|
||||
{data.name}
|
||||
</Text>
|
||||
</Box>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -113,11 +110,11 @@ function ModuleView() {
|
||||
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet
|
||||
}}
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
|
||||
{listImageState.findMany.data?.map((item) => (
|
||||
<ModuleItem key={item.id} data={item} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
|
||||
{listImageState.findMany.data?.map((item) => (
|
||||
<ModuleItem key={item.id} data={item} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,17 +30,41 @@ export default function ProfileView({ data }: ProfileViewProps) {
|
||||
justify="end"
|
||||
align="end"
|
||||
pos="relative"
|
||||
w={{ base: '100%', md: '40%' }}
|
||||
px="xl"
|
||||
w={{
|
||||
base: '100%', // mobile: full width
|
||||
xs: '100%', // small mobile
|
||||
sm: '85%', // tablet: 85%
|
||||
md: '60%', // laptop: 60%
|
||||
lg: '55%', // laptop large: 55%
|
||||
xl: '50%' // extra large (4K): 50%
|
||||
}}
|
||||
px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }}
|
||||
h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }}
|
||||
>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Foto profil'}
|
||||
fit="cover"
|
||||
radius="lg"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Box
|
||||
pos="relative"
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Foto profil'}
|
||||
fit="contain"
|
||||
radius="lg"
|
||||
loading="lazy"
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{
|
||||
objectPosition: 'center bottom',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack align="center" gap="xs" w="100%" py="xl">
|
||||
<IconUserCircle size={96} stroke={1.5} />
|
||||
@@ -49,24 +73,48 @@ export default function ProfileView({ data }: ProfileViewProps) {
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
<Box pos="absolute" bottom={0} w="100%" p={{ base: 'xs', md: 'md' }}>
|
||||
|
||||
{/* Box nama dan jabatan - responsive positioning */}
|
||||
<Box
|
||||
pos="absolute"
|
||||
bottom={{ base: -30, sm: -25, md: -20 }}
|
||||
right={0}
|
||||
w={{ base: '95%', sm: '100%' }}
|
||||
px={{ base: 'xs', sm: 'sm', md: 'md' }}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<Card
|
||||
px="lg"
|
||||
radius="2xl"
|
||||
px={{ base: 'md', sm: 'lg' }}
|
||||
py={{ base: 'xs', sm: 'sm' }}
|
||||
radius="lg"
|
||||
withBorder
|
||||
className="glass3"
|
||||
style={{ border: '1px solid rgba(255,255,255,0.15)' }}
|
||||
style={{
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
pointerEvents: 'auto',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Jabatan Resmi" withArrow>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text
|
||||
fz={{ base: 'xs', sm: 'sm' }}
|
||||
c="dimmed"
|
||||
lineClamp={1}
|
||||
>
|
||||
{data.position || 'Tidak ada jabatan'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Text c={colors['blue-button']} fw={700} fz="xl" mt={4}>
|
||||
<Text
|
||||
c={colors['blue-button']}
|
||||
fw={700}
|
||||
fz={{ base: 'lg', sm: 'xl' }}
|
||||
mt={4}
|
||||
lineClamp={2}
|
||||
>
|
||||
{data.name}
|
||||
</Text>
|
||||
</Card>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,27 @@
|
||||
"use client";
|
||||
import colors from "@/con/colors";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Skeleton,
|
||||
Center,
|
||||
Flex,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Center,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from "@mantine/core";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ModuleView from "./ModuleView";
|
||||
import SosmedView from "./SosmedView";
|
||||
import ProfileView from "./ProfileView";
|
||||
import SosmedView from "./SosmedView";
|
||||
|
||||
const getDayOfWeek = () => {
|
||||
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
|
||||
@@ -120,23 +121,21 @@ function LandingPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack bg={colors.Bg} p="md" gap="xl">
|
||||
<Stack bg={colors.Bg} p="md" gap="lg">
|
||||
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}>
|
||||
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
|
||||
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
|
||||
<Stack gap="xl">
|
||||
<Flex gap="md" wrap="wrap">
|
||||
<Group>
|
||||
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
||||
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
|
||||
</Box>
|
||||
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
||||
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
|
||||
</Box>
|
||||
</Group>
|
||||
<Grid w="100%">
|
||||
<Grid.Col span={{ base: 3, sm: 2 }}>
|
||||
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
||||
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 9, sm: 10 }}>
|
||||
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
||||
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Paper
|
||||
bg={colors["blue-button"]}
|
||||
@@ -199,7 +198,7 @@ function LandingPage() {
|
||||
)}
|
||||
|
||||
<Text ta="center" c={colors.trans.dark[2]}>
|
||||
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
|
||||
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
|
||||
Semua lebih mudah dengan fitur interaktif yang kami sediakan.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -30,17 +30,13 @@ const textHeading = {
|
||||
function Layanan() {
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.grey[1]} gap={"42"} py={"xl"}>
|
||||
<Container w={{ base: "100%", md: "50%" }} p={"xl"}>
|
||||
<Stack pos={"relative"} bg={colors.grey[1]} gap={"xl"} py={"md"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
|
||||
<Stack align="center" gap={"0"}>
|
||||
<Text fz={"3.4rem"} fw={"bold"}>
|
||||
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>
|
||||
{textHeading.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||
{textHeading.des}
|
||||
</Text>
|
||||
<Box p={"md"}>
|
||||
|
||||
@@ -68,7 +68,7 @@ function Penghargaan() {
|
||||
variant="gradient"
|
||||
gradient={{ from: "cyan", to: "blue", deg: 60 }}
|
||||
>
|
||||
Penghargaan & Prestasi Desa
|
||||
Penghargaan Desa
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BackgroundImage,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
@@ -48,15 +49,15 @@ function Potensi() {
|
||||
const data = (state.findMany.data || []).slice(0, 4);
|
||||
|
||||
return (
|
||||
<Stack p="sm" gap="4rem">
|
||||
<Box>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }} fw={700} c={colors["blue-button"]}>
|
||||
<Stack p="sm" gap="xl">
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
|
||||
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>
|
||||
{textHeading.title}
|
||||
</Text>
|
||||
<Text ta={"center"} fz={{ base: "1.4rem", md: "1.6rem" }} c="black">
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||
{textHeading.des}
|
||||
</Text>
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{loading ? (
|
||||
<Stack align="center" justify="center" h={300}>
|
||||
|
||||
@@ -35,7 +35,7 @@ function Prestasi() {
|
||||
<Stack align="center" gap="sm">
|
||||
<Group gap="xs">
|
||||
<IconTrophy size={36} color={colors["blue-button"]} />
|
||||
<Text ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
|
||||
<Text c={colors["blue-button"]} ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
|
||||
Prestasi Desa
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -55,7 +55,7 @@ function Prestasi() {
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
<Box py={50}>
|
||||
<Box py="lg">
|
||||
{loading ? (
|
||||
<Center mih={200}>
|
||||
<Loader color={colors["blue-button"]} size="xl" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMediaQuery } from "@mantine/hooks"
|
||||
import { Prisma } from "@prisma/client"
|
||||
import Link from "next/link"
|
||||
import { IconMoodSad } from "@tabler/icons-react"
|
||||
import colors from "@/con/colors"
|
||||
|
||||
export default function SDGS() {
|
||||
const theme = useMantineTheme()
|
||||
@@ -41,20 +42,16 @@ export default function SDGS() {
|
||||
order={1}
|
||||
fz={{ base: "2.4rem", md: "3.6rem" }}
|
||||
fw={900}
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #1A5F7A, #159895)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
}}
|
||||
c={colors["blue-button"]}
|
||||
>
|
||||
SDGs Desa
|
||||
</Title>
|
||||
</Center>
|
||||
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
|
||||
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan: dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
|
||||
</Text>
|
||||
|
||||
<Box py={50}>
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
p={{ base: "md", md: "xl" }}
|
||||
radius="2xl"
|
||||
@@ -67,49 +64,62 @@ export default function SDGS() {
|
||||
>
|
||||
{sdgsDesa && sdgsDesa.length > 0 ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{sdgsDesa.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
|
||||
border: "1px solid rgba(0,0,0,0.05)",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<Center mb="lg">
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
background: "rgba(240, 249, 255, 0.8)",
|
||||
backdropFilter: "blur(6px)",
|
||||
width: mobile ? 140 : 160,
|
||||
height: mobile ? 140 : 160,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
|
||||
alt={item.name}
|
||||
w={mobile ? 90 : 110}
|
||||
h={mobile ? 90 : 110}
|
||||
fit="contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
{sdgsDesa.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
|
||||
border: "1px solid rgba(0,0,0,0.05)",
|
||||
transition: "all 0.3s ease",
|
||||
height: "100%", // biar tinggi antar card konsisten
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Center mb="lg">
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
background: "rgba(240, 249, 255, 0.8)",
|
||||
backdropFilter: "blur(6px)",
|
||||
width: mobile ? 140 : 160,
|
||||
height: mobile ? 140 : 160,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
|
||||
alt={item.name}
|
||||
w={mobile ? 90 : 110}
|
||||
h={mobile ? 90 : 110}
|
||||
fit="contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
{/* Stack isi teks & angka */}
|
||||
<Stack justify="space-between" align="center" gap="xs" h="100%">
|
||||
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
|
||||
<Text ta="center" fz={{ base: "lg", md: "xl" }} fw={700} mb="xs">
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: "lg", md: "xl" }}
|
||||
fw={700}
|
||||
mb="xs"
|
||||
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
||||
<Title
|
||||
order={2}
|
||||
ta="center"
|
||||
@@ -122,9 +132,11 @@ export default function SDGS() {
|
||||
>
|
||||
{item.jumlah}
|
||||
</Title>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
) : (
|
||||
<Center mih={200} style={{ flexDirection: "column" }}>
|
||||
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
|
||||
@@ -141,7 +153,7 @@ export default function SDGS() {
|
||||
href="/darmasaba/sdgs-desa"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
mt={40}
|
||||
mt="md"
|
||||
variant="gradient"
|
||||
gradient={{ from: "#26667F", to: "#124170" }}
|
||||
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}}
|
||||
|
||||
36
src/app/darmasaba/_com/scrollToTopButton.tsx
Normal file
36
src/app/darmasaba/_com/scrollToTopButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import { useWindowScroll } from '@mantine/hooks';
|
||||
import { ActionIcon, Transition } from '@mantine/core';
|
||||
import { IconArrowUp } from '@tabler/icons-react';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
function ScrollToTopButton() {
|
||||
const [scroll, scrollTo] = useWindowScroll();
|
||||
|
||||
return (
|
||||
<Transition
|
||||
mounted={scroll.y > 300}
|
||||
transition="slide-up"
|
||||
duration={300}
|
||||
timingFunction="ease"
|
||||
>
|
||||
{(styles) => (
|
||||
<ActionIcon
|
||||
style={styles}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
variant="filled"
|
||||
color={colors['blue-button']}
|
||||
onClick={() => scrollTo({ y: 0 })}
|
||||
pos="fixed"
|
||||
bottom={24}
|
||||
right={24}
|
||||
aria-label="Kembali ke atas"
|
||||
>
|
||||
<IconArrowUp size={20} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
export default ScrollToTopButton
|
||||
92
src/app/darmasaba/_com/searchUrl.tsx
Normal file
92
src/app/darmasaba/_com/searchUrl.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
||||
const { type, id, kategori } = item;
|
||||
const map: Record<string, (id: string | number, kategori?: string) => string> = {
|
||||
programinovasi: (id) => `/darmasaba/program-inovasi/${id}`,
|
||||
desaantikorupsi: () => '/darmasaba/desa-anti-korupsi',
|
||||
sdgsdesa: () => '/darmasaba/sdgs-desa',
|
||||
apbdes: () => '/darmasaba/apbdes',
|
||||
prestasidesa: () => '/darmasaba/prestasi-desa',
|
||||
pejabatdesa: () => '/darmasaba/ppid/profile-ppid',
|
||||
strukturppid: () => '/darmasaba/ppid/struktur-ppid',
|
||||
visimisippid: () => '/darmasaba/ppid/visi-misi',
|
||||
dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum',
|
||||
profileppid: () => '/darmasaba/ppid/profile',
|
||||
daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik',
|
||||
perbekeldarmasaba: () => '/darmasaba/desa/profile',
|
||||
berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`,
|
||||
pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`,
|
||||
sejarahdesa: () => '/darmasaba/desa/profile',
|
||||
visimisidesa: () => '/darmasaba/desa/profile',
|
||||
lambangdesa: () => '/darmasaba/desa/profile',
|
||||
maskotdesa: () => '/darmasaba/desa/profile',
|
||||
profilperbekel: () => '/darmasaba/desa/profile',
|
||||
potensi: () => '/darmasaba/desa/potensi-desa',
|
||||
galleryFoto: () => '/darmasaba/desa/gallery/foto',
|
||||
galleryVideo: () => '/darmasaba/desa/gallery/video',
|
||||
pelayananSuratKeterangan: () => '/darmasaba/desa/layanan',
|
||||
pelayananPerizinanBerusaha: () => '/darmasaba/desa/layanan',
|
||||
pelayananTelunjukSaktiDesa: () => '/darmasaba/desa/layanan',
|
||||
pelayananPendudukNonPermanent: () => '/darmasaba/desa/layanan',
|
||||
penghargaan: () => '/darmasaba/desa/penghargaan',
|
||||
posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`,
|
||||
fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||
jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||
artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||
puskesmas: () => '/darmasaba/kesehatan/puskesmas',
|
||||
programKesehatan: () => '/darmasaba/kesehatan/program-kesehatan',
|
||||
penangananDarurat: () => '/darmasaba/kesehatan/penanganan-darurat',
|
||||
kontakDarurat: () => '/darmasaba/kesehatan/kontak-darurat',
|
||||
infoWabahPenyakit: () => '/darmasaba/kesehatan/info-wabah-penyakit',
|
||||
keamananLingkungan: () => '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
|
||||
polsekTerdekat: () => '/darmasaba/keamanan/polsek-terdekat',
|
||||
kontakDaruratKeamanan: () => '/darmasaba/keamanan/kontak-darurat',
|
||||
pencegahanKriminalitas: () => '/darmasaba/keamanan/pencegahan-kriminalitas',
|
||||
laporanPublik: () => '/darmasaba/keamanan/laporan-publik',
|
||||
tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan',
|
||||
pasarDesa: () => '/darmasaba/ekonomi/pasar-desa',
|
||||
lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal',
|
||||
strukturOrganisasi: () => '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
|
||||
jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||
jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin',
|
||||
programKemiskinan: () => '/darmasaba/ekonomi/program-kemiskinan',
|
||||
sektorUnggulanDesa: () => '/darmasaba/ekonomi/sektor-unggulan-desa',
|
||||
demografiPekerjaan: () => '/darmasaba/ekonomi/demografi-pekerjaan',
|
||||
desaDigital: () => '/darmasaba/inovasi/desa-digital-smart-village',
|
||||
programKreatif: () => '/darmasaba/inovasi/program-kreatif-desa',
|
||||
kolaborasiInovasi: () => '/darmasaba/inovasi/kolaborasi-inovasi',
|
||||
mitraKolaborasi: () => '/darmasaba/inovasi/kolaborasi-inovasi',
|
||||
infoTekno: () => '/darmasaba/inovasi/info-teknologi-tepat-guna',
|
||||
pengelolaanSampah: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
|
||||
keteranganBankSampahTerdekat: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
|
||||
programPenghijauan: () => '/darmasaba/lingkungan/program-penghijauan',
|
||||
dataLingkunganDesa: () => '/darmasaba/lingkungan/data-lingkungan-desa',
|
||||
gotongRoyong: (id, kategori) => `/darmasaba/lingkungan/gotong-royong/${kategori}/${id}`,
|
||||
tujuanEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
|
||||
materiEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
|
||||
contohEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
|
||||
filosofiTriHita: () => '/darmasaba/lingkungan/konservasi-adat-bali',
|
||||
bentukKonservasiBerdasarkanAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali',
|
||||
nilaiKonservasiAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali',
|
||||
jenjangPendidikan: () => '/darmasaba/pendidikan/info-sekolah/semua',
|
||||
lembaga: () => '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
|
||||
siswa: () => '/darmasaba/pendidikan/info-sekolah/semua/siswa',
|
||||
pengajar: () => '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
|
||||
keunggulanProgram: () => '/darmasaba/pendidikan/beasiswa-desa',
|
||||
tujuanProgram: () => '/darmasaba/pendidikan/program-pendidikan-anak',
|
||||
programUnggulan: () => '/darmasaba/pendidikan/program-pendidikan-anak',
|
||||
lokasiJadwalBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa',
|
||||
fasilitasBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa',
|
||||
tujuanPendidikanNonFormal: () => '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||
tempatKegiatan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||
jenisProgramYangDiselenggarakan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||
dataPerpustakaan: () => '/darmasaba/pendidikan/perpustakaan-digital/semua',
|
||||
dataPendidikan: () => '/darmasaba/pendidikan/data-pendidikan',
|
||||
|
||||
};
|
||||
|
||||
if (type && map[type]) return map[type](id, kategori as string | undefined);
|
||||
return '/darmasaba';
|
||||
};
|
||||
|
||||
export default getDetailUrl;
|
||||
@@ -8,23 +8,31 @@ import colors from "@/con/colors";
|
||||
import SDGS from "./_com/main-page/sdgs";
|
||||
// import ApiFetch from "@/lib/api-fetch";
|
||||
|
||||
import { Stack } from "@mantine/core";
|
||||
import { Box, Stack } from "@mantine/core";
|
||||
import Apbdes from "./_com/main-page/apbdes";
|
||||
import Prestasi from "./_com/main-page/prestasi";
|
||||
import ScrollToTopButton from "./_com/scrollToTopButton";
|
||||
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Stack bg={colors.grey[1]} gap={"4rem"}>
|
||||
<LandingPage />
|
||||
<Penghargaan />
|
||||
<Layanan />
|
||||
<Potensi />
|
||||
<DesaAntiKorupsi />
|
||||
<Kepuasan />
|
||||
<SDGS />
|
||||
<Apbdes />
|
||||
<Prestasi/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
bg={colors.grey[1]}
|
||||
gap={"1.5rem"}
|
||||
>
|
||||
<LandingPage />
|
||||
<Penghargaan />
|
||||
<Layanan />
|
||||
<Potensi />
|
||||
<DesaAntiKorupsi />
|
||||
<Kepuasan />
|
||||
<SDGS />
|
||||
<Apbdes />
|
||||
<Prestasi />
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
backdrop-filter: blur(40px);
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
const colors = {
|
||||
"orange": "#FCAE00",
|
||||
"blue-button": "#0A4E78",
|
||||
"blue-button-1": "#E5F2FA",
|
||||
"blue-button-2": "#B8DAEF",
|
||||
"blue-button-3": "#8AC1E3",
|
||||
"blue-button-4": "#5DA9D8",
|
||||
"blue-button-5": "#2F91CC",
|
||||
"blue-button-6": "#083F61",
|
||||
"blue-button-7": "#062F49",
|
||||
"blue-button-8": "#041F32",
|
||||
"blue-button-trans": "#628EC6",
|
||||
"white-1": "#FBFBFC",
|
||||
"white-trans-1": "rgba(255, 255, 255, 0.5)",
|
||||
|
||||
@@ -2,7 +2,6 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "1",
|
||||
name: "PPID",
|
||||
href: "/darmasaba/ppid/profile-ppid",
|
||||
children: [
|
||||
{
|
||||
id: "1.1",
|
||||
@@ -51,7 +50,6 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "2",
|
||||
name: "Desa",
|
||||
href: "/darmasaba/desa/profile",
|
||||
children: [
|
||||
{
|
||||
id: "2.1",
|
||||
@@ -94,7 +92,6 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "3",
|
||||
name: "Kesehatan",
|
||||
href: "/darmasaba/kesehatan/posyandu",
|
||||
children: [
|
||||
{
|
||||
id: "3.1",
|
||||
@@ -136,7 +133,6 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "4",
|
||||
name: "Keamanan",
|
||||
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
|
||||
children: [
|
||||
{
|
||||
id: "4.1",
|
||||
@@ -173,7 +169,6 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "5",
|
||||
name: "Ekonomi",
|
||||
href: "/darmasaba/ekonomi/pasar-desa",
|
||||
children: [
|
||||
{
|
||||
id: "5.1",
|
||||
@@ -229,7 +224,6 @@ const navbarListMenu = [
|
||||
}, {
|
||||
id: "6",
|
||||
name: "Inovasi",
|
||||
href: "/darmasaba/inovasi/desa-digital-smart-village",
|
||||
children: [
|
||||
{
|
||||
id: "6.1",
|
||||
@@ -266,7 +260,6 @@ const navbarListMenu = [
|
||||
}, {
|
||||
id: "7",
|
||||
name: "Lingkungan",
|
||||
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
|
||||
children: [
|
||||
{
|
||||
id: "7.1",
|
||||
@@ -302,7 +295,6 @@ const navbarListMenu = [
|
||||
}, {
|
||||
id: "8",
|
||||
name: "Pendidikan",
|
||||
href: "/darmasaba/pendidikan/info-sekolah",
|
||||
children: [
|
||||
{
|
||||
id: "8.1",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export type MenuItem = {
|
||||
id: string,
|
||||
name: string,
|
||||
href: string,
|
||||
children?: MenuItem[]
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
href?: string;
|
||||
children?: MenuItem[];
|
||||
} & (
|
||||
{ href: string; children?: MenuItem[] } |
|
||||
{ children: MenuItem[] }
|
||||
)
|
||||
Reference in New Issue
Block a user