Compare commits
11 Commits
nico/15-ok
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d66a952d4c | |||
| ed371bd0d9 | |||
| f82c7b86e0 | |||
| b5d6585cd5 | |||
| aa98359ef7 | |||
| 0ff0d5234a | |||
| 827c1c191a | |||
| fb596f9033 | |||
| 9055b40769 | |||
| bbf13c1cf7 | |||
| 75bf0652b1 |
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun next dev --hostname 0.0.0.0",
|
"dev": "bun --bun next dev",
|
||||||
"build": "bun --bun next build",
|
"build": "bun --bun next build",
|
||||||
"start": "bun --bun next start"
|
"start": "bun --bun next start"
|
||||||
},
|
},
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
|
"@types/nodemailer": "^7.0.2",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel-autoplay": "^8.5.2",
|
"embla-carousel-autoplay": "^8.5.2",
|
||||||
"embla-carousel-react": "^7.1.0",
|
"embla-carousel-react": "^7.1.0",
|
||||||
@@ -71,6 +73,7 @@
|
|||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
"next-view-transitions": "^0.3.4",
|
"next-view-transitions": "^0.3.4",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"nodemailer": "^7.0.10",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.6",
|
"primereact": "^10.9.6",
|
||||||
@@ -82,6 +85,7 @@
|
|||||||
"react-simple-toasts": "^6.1.0",
|
"react-simple-toasts": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"sharp": "^0.34.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",
|
"id": "cmds9023u0008vnbe3oxmhwyf",
|
||||||
"name": "Desa Darmasaba",
|
"name": "Desa Darmasaba",
|
||||||
|
|||||||
@@ -1606,7 +1606,7 @@ model Pembiayaan {
|
|||||||
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
|
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= INOVASI ========================================= //
|
// ========================================= MENU INOVASI ========================================= //
|
||||||
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
||||||
model DesaDigital {
|
model DesaDigital {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 378 KiB |
@@ -75,7 +75,8 @@ const berita = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
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.page = page;
|
||||||
berita.findMany.search = search;
|
berita.findMany.search = search;
|
||||||
|
|
||||||
@@ -98,7 +99,14 @@ const berita = proxy({
|
|||||||
berita.findMany.data = [];
|
berita.findMany.data = [];
|
||||||
berita.findMany.totalPages = 1;
|
berita.findMany.totalPages = 1;
|
||||||
} finally {
|
} 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(() => {
|
||||||
berita.findMany.loading = false;
|
berita.findMany.loading = false;
|
||||||
|
}, delay);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const dataPerpustakaan = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
|
const startTime = Date.now();
|
||||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
dataPerpustakaan.findMany.page = page;
|
dataPerpustakaan.findMany.page = page;
|
||||||
dataPerpustakaan.findMany.search = search;
|
dataPerpustakaan.findMany.search = search;
|
||||||
@@ -77,7 +78,10 @@ const dataPerpustakaan = proxy({
|
|||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
if (kategori) query.kategori = kategori;
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||||
@@ -91,7 +95,14 @@ const dataPerpustakaan = proxy({
|
|||||||
dataPerpustakaan.findMany.data = [];
|
dataPerpustakaan.findMany.data = [];
|
||||||
dataPerpustakaan.findMany.totalPages = 1;
|
dataPerpustakaan.findMany.totalPages = 1;
|
||||||
} finally {
|
} 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;
|
dataPerpustakaan.findMany.loading = false;
|
||||||
|
}, delay);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -115,7 +126,10 @@ const dataPerpustakaan = proxy({
|
|||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
if (kategori) query.kategori = kategori;
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findManyAll"].get({ query });
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||||
|
"findManyAll"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||||
@@ -365,7 +379,10 @@ const kategoriBuku = proxy({
|
|||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
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) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
kategoriBuku.findMany.data = res.data.data ?? [];
|
kategoriBuku.findMany.data = res.data.data ?? [];
|
||||||
@@ -557,7 +574,7 @@ const templatePeminjamanBuku = z.object({
|
|||||||
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
|
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
|
||||||
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
|
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
|
||||||
tanggalKembali: z.string().min(1, "Tanggal 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 = {
|
const defaultPeminjamanBuku = {
|
||||||
@@ -568,7 +585,7 @@ const defaultPeminjamanBuku = {
|
|||||||
tanggalPinjam: "",
|
tanggalPinjam: "",
|
||||||
batasKembali: "",
|
batasKembali: "",
|
||||||
tanggalKembali: "",
|
tanggalKembali: "",
|
||||||
catatan: ""
|
catatan: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FormEditData {
|
interface FormEditData {
|
||||||
@@ -584,7 +601,7 @@ interface FormEditData {
|
|||||||
batasKembali: string;
|
batasKembali: string;
|
||||||
tanggalKembali: string;
|
tanggalKembali: string;
|
||||||
catatan: string;
|
catatan: string;
|
||||||
status: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan';
|
status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
|
||||||
}
|
}
|
||||||
|
|
||||||
const editForm: FormEditData = {
|
const editForm: FormEditData = {
|
||||||
@@ -596,8 +613,8 @@ const editForm: FormEditData = {
|
|||||||
batasKembali: "",
|
batasKembali: "",
|
||||||
tanggalKembali: "",
|
tanggalKembali: "",
|
||||||
catatan: "",
|
catatan: "",
|
||||||
status: "Dipinjam"
|
status: "Dipinjam",
|
||||||
}
|
};
|
||||||
|
|
||||||
const peminjamanBuku = proxy({
|
const peminjamanBuku = proxy({
|
||||||
create: {
|
create: {
|
||||||
@@ -651,7 +668,10 @@ const peminjamanBuku = proxy({
|
|||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
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) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
peminjamanBuku.findMany.data = res.data.data ?? [];
|
peminjamanBuku.findMany.data = res.data.data ?? [];
|
||||||
@@ -720,7 +740,9 @@ const peminjamanBuku = proxy({
|
|||||||
);
|
);
|
||||||
await peminjamanBuku.findMany.load(); // refresh list
|
await peminjamanBuku.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus Data Peminjaman Buku");
|
toast.error(
|
||||||
|
result?.message || "Gagal menghapus Data Peminjaman Buku"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
console.error("Gagal delete:", error);
|
||||||
@@ -768,7 +790,7 @@ const peminjamanBuku = proxy({
|
|||||||
batasKembali: data.batasKembali,
|
batasKembali: data.batasKembali,
|
||||||
tanggalKembali: data.tanggalKembali,
|
tanggalKembali: data.tanggalKembali,
|
||||||
catatan: data.catatan,
|
catatan: data.catatan,
|
||||||
status: data.status
|
status: data.status,
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -811,7 +833,7 @@ const peminjamanBuku = proxy({
|
|||||||
batasKembali: this.form.batasKembali,
|
batasKembali: this.form.batasKembali,
|
||||||
tanggalKembali: this.form.tanggalKembali,
|
tanggalKembali: this.form.tanggalKembali,
|
||||||
catatan: this.form.catatan,
|
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
|
await peminjamanBuku.findMany.load(); // refresh list
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal update data peminjaman buku");
|
throw new Error(
|
||||||
|
result.message || "Gagal update data peminjaman buku"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating data peminjaman buku:", error);
|
console.error("Error updating data peminjaman buku:", error);
|
||||||
@@ -849,7 +873,7 @@ const peminjamanBuku = proxy({
|
|||||||
peminjamanBuku.update.form = { ...editForm };
|
peminjamanBuku.update.form = { ...editForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const perpustakaanDigitalState = proxy({
|
const perpustakaanDigitalState = proxy({
|
||||||
dataPerpustakaan,
|
dataPerpustakaan,
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
stateProgram.findUnique
|
const loadData = async () => {
|
||||||
.load(id)
|
try {
|
||||||
.then(() => {
|
await stateProgram.findUnique.load(id);
|
||||||
const data = stateProgram.findUnique.data;
|
const data = stateProgram.findUnique.data;
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
|
||||||
console.error('Error load data:', err);
|
console.error('Error load data:', err);
|
||||||
toast.error('Gagal mengambil data program');
|
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
|
// generic handler untuk field top-level
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ function EditArtikelKesehatan() {
|
|||||||
{/* Gambar */}
|
{/* Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
Gambar Berita
|
Gambar Artikel Kesehatan
|
||||||
</Text>
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={handleFileChange}
|
onDrop={handleFileChange}
|
||||||
@@ -240,15 +240,15 @@ function EditArtikelKesehatan() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pendahuluan */}
|
{/* Pendahuluan */}
|
||||||
<InputText
|
<Box>
|
||||||
label="Pendahuluan"
|
<Text fw="bold">Pendahuluan</Text>
|
||||||
|
<EditEditor
|
||||||
value={formData.introduction.content}
|
value={formData.introduction.content}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setFormData((prev) => ({ ...prev, introduction: { content: value } }))
|
setFormData((prev) => ({ ...prev, introduction: { ...prev.introduction, content: value } }))
|
||||||
}
|
}
|
||||||
placeholder="Masukkan pendahuluan"
|
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
{/* Gejala */}
|
{/* Gejala */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold">Gejala</Text>
|
<Text fw="bold">Gejala</Text>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ function CreateArtikelKesehatan() {
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
Gambar Berita
|
Gambar Artikel Kesehatan
|
||||||
</Text>
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
@@ -182,16 +182,15 @@ function CreateArtikelKesehatan() {
|
|||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<Box>
|
||||||
label={"Pendahuluan"}
|
<Text fz="sm" fw="bold">Pendahuluan</Text>
|
||||||
placeholder="Masukkan pendahuluan"
|
<CreateEditor
|
||||||
required
|
value={stateArtikelKesehatan.create.form.introduction.content}
|
||||||
defaultValue={stateArtikelKesehatan.create.form.introduction.content}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
stateArtikelKesehatan.create.form.introduction.content = e.target.value;
|
stateArtikelKesehatan.create.form.introduction.content = e;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
{/* Gejala */}
|
{/* Gejala */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="md" fw="bold">Gejala</Text>
|
<Text fz="md" fw="bold">Gejala</Text>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push("/login");
|
router.push("/darmasaba");
|
||||||
}}
|
}}
|
||||||
color={colors["blue-button"]}
|
color={colors["blue-button"]}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
|||||||
@@ -701,6 +701,457 @@ export default async function searchFindMany(context: Context) {
|
|||||||
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================= MENU INOVASI ========================================= //
|
||||||
|
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
||||||
|
|
||||||
|
if (type === "desaDigital") {
|
||||||
|
const data = await prisma.desaDigital.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= PROGRAM KREATIF ========================================= //
|
||||||
|
|
||||||
|
if (type === "programKreatif") {
|
||||||
|
const data = await prisma.programKreatif.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ slug: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= KOLABORASI INOVASI ========================================= //
|
||||||
|
|
||||||
|
if (type === "kolaborasiInovasi") {
|
||||||
|
const data = await prisma.kolaborasiInovasi.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ slug: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } },
|
||||||
|
{ kolaborator: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "mitraKolaborasi") {
|
||||||
|
const data = await prisma.mitraKolaborasi.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
|
||||||
|
|
||||||
|
if (type === "infoTekno") {
|
||||||
|
const data = await prisma.infoTekno.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= LINGKUNGAN ========================================= //
|
||||||
|
// ========================================= PENGELOLAAN SAMPAH ========================================= //
|
||||||
|
|
||||||
|
if (type === "pengelolaanSampah") {
|
||||||
|
const data = await prisma.pengelolaanSampah.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "keteranganBankSampahTerdekat") {
|
||||||
|
const data = await prisma.keteranganBankSampahTerdekat.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ alamat: { contains: query, mode: "insensitive" } },
|
||||||
|
{ namaTempatMaps: { contains: query, mode: "insensitive" } },
|
||||||
|
{ linkPetunjukArah: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= PORGRAM PENGHIJAUAN ========================================= //
|
||||||
|
|
||||||
|
if (type === "programPenghijauan") {
|
||||||
|
const data = await prisma.programPenghijauan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= DATA LINGKUNGAN DESA ========================================= //
|
||||||
|
|
||||||
|
if (type === "dataLingkunganDesa") {
|
||||||
|
const data = await prisma.dataLingkunganDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= GOTONG ROYONG ========================================= //
|
||||||
|
|
||||||
|
if (type === "gotongRoyong") {
|
||||||
|
const data = await prisma.kegiatanDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsiLengkap: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= EDUKASI LINGKUNGAN ========================================= //
|
||||||
|
|
||||||
|
if (type === "tujuanEdukasiLingkungan") {
|
||||||
|
const data = await prisma.tujuanEdukasiLingkungan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "materiEdukasiLingkungan") {
|
||||||
|
const data = await prisma.materiEdukasiLingkungan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "contohEdukasiLingkungan") {
|
||||||
|
const data = await prisma.contohEdukasiLingkungan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= KONSERVASI ADAT BALI ========================================= //
|
||||||
|
|
||||||
|
if (type === "filosofiTriHita") {
|
||||||
|
const data = await prisma.filosofiTriHita.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "bentukKonservasiBerdasarkanAdat") {
|
||||||
|
const data = await prisma.bentukKonservasiBerdasarkanAdat.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "nilaiKonservasiAdat") {
|
||||||
|
const data = await prisma.nilaiKonservasiAdat.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= MENU PENDIDIKAN ========================================= //
|
||||||
|
// ========================================= INFO SEKOLAH & PAUD ========================================= //
|
||||||
|
|
||||||
|
if (type === "jenjangPendidikan") {
|
||||||
|
const data = await prisma.jenjangPendidikan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "lembaga") {
|
||||||
|
const data = await prisma.lembaga.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "siswa") {
|
||||||
|
const data = await prisma.siswa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "pengajar") {
|
||||||
|
const data = await prisma.pengajar.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= BEASISWA DESA ========================================= //
|
||||||
|
if (type === "keunggulanProgram") {
|
||||||
|
const data = await prisma.keunggulanProgram.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
|
||||||
|
if (type === "tujuanProgram") {
|
||||||
|
const data = await prisma.tujuanProgram.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "programUnggulan") {
|
||||||
|
const data = await prisma.programUnggulan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "lokasiJadwalBimbinganBelajarDesa") {
|
||||||
|
const data = await prisma.lokasiJadwalBimbinganBelajarDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "fasilitasBimbinganBelajarDesa") {
|
||||||
|
const data = await prisma.fasilitasBimbinganBelajarDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
|
||||||
|
if (type === "tujuanPendidikanNonFormal") {
|
||||||
|
const data = await prisma.tujuanPendidikanNonFormal.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "tempatKegiatan") {
|
||||||
|
const data = await prisma.tempatKegiatan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "jenisProgramYangDiselenggarakan") {
|
||||||
|
const data = await prisma.jenisProgramYangDiselenggarakan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= PERPUSTAKAAN ========================================= //
|
||||||
|
if (type === "dataPerpustakaan") {
|
||||||
|
const data = await prisma.dataPerpustakaan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||||
|
if (type === "dataPendidikan") {
|
||||||
|
const data = await prisma.dataPendidikan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ jumlah: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limitNum,
|
||||||
|
});
|
||||||
|
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
// 🌍 GLOBAL SEARCH — cari di beberapa modul sekaligus
|
// 🌍 GLOBAL SEARCH — cari di beberapa modul sekaligus
|
||||||
const [
|
const [
|
||||||
pejabatdesa,
|
pejabatdesa,
|
||||||
@@ -760,6 +1211,37 @@ export default async function searchFindMany(context: Context) {
|
|||||||
programKemiskinan,
|
programKemiskinan,
|
||||||
sektorUnggulanDesa,
|
sektorUnggulanDesa,
|
||||||
demografiPekerjaan,
|
demografiPekerjaan,
|
||||||
|
desaDigital,
|
||||||
|
programKreatif,
|
||||||
|
kolaborasiInovasi,
|
||||||
|
mitraKolaborasi,
|
||||||
|
infoTekno,
|
||||||
|
pengelolaanSampah,
|
||||||
|
keteranganBankSampahTerdekat,
|
||||||
|
programPenghijauan,
|
||||||
|
dataLingkunganDesa,
|
||||||
|
gotongRoyong,
|
||||||
|
tujuanEdukasiLingkungan,
|
||||||
|
materiEdukasiLingkungan,
|
||||||
|
contohEdukasiLingkungan,
|
||||||
|
filosofiTriHita,
|
||||||
|
bentukKonservasiBerdasarkanAdat,
|
||||||
|
nilaiKonservasiAdat,
|
||||||
|
jenjangPendidikan,
|
||||||
|
lembaga,
|
||||||
|
siswa,
|
||||||
|
pengajar,
|
||||||
|
keunggulanProgram,
|
||||||
|
tujuanProgram,
|
||||||
|
programUnggulan,
|
||||||
|
lokasiJadwalBimbinganBelajarDesa,
|
||||||
|
fasilitasBimbinganBelajarDesa,
|
||||||
|
tujuanPendidikanNonFormal,
|
||||||
|
tempatKegiatan,
|
||||||
|
jenisProgramYangDiselenggarakan,
|
||||||
|
dataPerpustakaan,
|
||||||
|
dataPendidikan
|
||||||
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.pejabatDesa.findMany({
|
prisma.pejabatDesa.findMany({
|
||||||
where: { name: { contains: query, mode: "insensitive" } },
|
where: { name: { contains: query, mode: "insensitive" } },
|
||||||
@@ -1097,6 +1579,277 @@ export default async function searchFindMany(context: Context) {
|
|||||||
pekerjaan: { contains: query, mode: "insensitive" }
|
pekerjaan: { contains: query, mode: "insensitive" }
|
||||||
},
|
},
|
||||||
take: limitNum,
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.desaDigital.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.programKreatif.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ slug: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.kolaborasiInovasi.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ slug: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } },
|
||||||
|
{ kolaborator: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.mitraKolaborasi.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.infoTekno.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.pengelolaanSampah.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.keteranganBankSampahTerdekat.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ alamat: { contains: query, mode: "insensitive" } },
|
||||||
|
{ namaTempatMaps: { contains: query, mode: "insensitive" } },
|
||||||
|
{ linkPetunjukArah: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.programPenghijauan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.dataLingkunganDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.kegiatanDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsiLengkap: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.tujuanEdukasiLingkungan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.materiEdukasiLingkungan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.contohEdukasiLingkungan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.filosofiTriHita.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.bentukKonservasiBerdasarkanAdat.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.nilaiKonservasiAdat.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.jenjangPendidikan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.lembaga.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.siswa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.pengajar.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nama: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.keunggulanProgram.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.tujuanProgram.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.programUnggulan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.lokasiJadwalBimbinganBelajarDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.fasilitasBimbinganBelajarDesa.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.tujuanPendidikanNonFormal.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.tempatKegiatan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.jenisProgramYangDiselenggarakan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.dataPerpustakaan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ judul: { contains: query, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
|
}),
|
||||||
|
prisma.dataPendidikan.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: query, mode: "insensitive" } },
|
||||||
|
{ jumlah: { contains: query, mode: "insensitive" } }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: limitNum,
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1180,6 +1933,36 @@ export default async function searchFindMany(context: Context) {
|
|||||||
...programKemiskinan.map((b) => ({ type: "programKemiskinan", ...b })),
|
...programKemiskinan.map((b) => ({ type: "programKemiskinan", ...b })),
|
||||||
...sektorUnggulanDesa.map((b) => ({ type: "sektorUnggulanDesa", ...b })),
|
...sektorUnggulanDesa.map((b) => ({ type: "sektorUnggulanDesa", ...b })),
|
||||||
...demografiPekerjaan.map((b) => ({ type: "demografiPekerjaan", ...b })),
|
...demografiPekerjaan.map((b) => ({ type: "demografiPekerjaan", ...b })),
|
||||||
|
...desaDigital.map((b) => ({ type: "desaDigital", ...b })),
|
||||||
|
...programKreatif.map((b) => ({ type: "programKreatif", ...b })),
|
||||||
|
...kolaborasiInovasi.map((b) => ({ type: "kolaborasiInovasi", ...b })),
|
||||||
|
...mitraKolaborasi.map((b) => ({ type: "mitraKolaborasi", ...b })),
|
||||||
|
...infoTekno.map((b) => ({ type: "infoTekno", ...b })),
|
||||||
|
...pengelolaanSampah.map((b) => ({ type: "pengelolaanSampah", ...b })),
|
||||||
|
...keteranganBankSampahTerdekat.map((b) => ({ type: "keteranganBankSampahTerdekat", ...b })),
|
||||||
|
...programPenghijauan.map((b) => ({ type: "programPenghijauan", ...b })),
|
||||||
|
...dataLingkunganDesa.map((b) => ({ type: "dataLingkunganDesa", ...b })),
|
||||||
|
...gotongRoyong.map((b) => ({ type: "gotongRoyong", ...b })),
|
||||||
|
...tujuanEdukasiLingkungan.map((b) => ({ type: "tujuanEdukasiLingkungan", ...b })),
|
||||||
|
...materiEdukasiLingkungan.map((b) => ({ type: "materiEdukasiLingkungan", ...b })),
|
||||||
|
...contohEdukasiLingkungan.map((b) => ({ type: "contohEdukasiLingkungan", ...b })),
|
||||||
|
...filosofiTriHita.map((b) => ({ type: "filosofiTriHita", ...b })),
|
||||||
|
...bentukKonservasiBerdasarkanAdat.map((b) => ({ type: "bentukKonservasiBerdasarkanAdat", ...b })),
|
||||||
|
...nilaiKonservasiAdat.map((b) => ({ type: "nilaiKonservasiAdat", ...b })),
|
||||||
|
...jenjangPendidikan.map((b) => ({ type: "jenjangPendidikan", ...b })),
|
||||||
|
...lembaga.map((b) => ({ type: "lembaga", ...b })),
|
||||||
|
...siswa.map((b) => ({ type: "siswa", ...b })),
|
||||||
|
...pengajar.map((b) => ({ type: "pengajar", ...b })),
|
||||||
|
...keunggulanProgram.map((b) => ({ type: "keunggulanProgram", ...b })),
|
||||||
|
...tujuanProgram.map((b) => ({ type: "tujuanProgram", ...b })),
|
||||||
|
...programUnggulan.map((b) => ({ type: "programUnggulan", ...b })),
|
||||||
|
...tujuanPendidikanNonFormal.map((b) => ({ type: "tujuanPendidikanNonFormal", ...b })),
|
||||||
|
...fasilitasBimbinganBelajarDesa.map((b) => ({ type: "fasilitasBimbinganBelajarDesa", ...b })),
|
||||||
|
...lokasiJadwalBimbinganBelajarDesa.map((b) => ({ type: "lokasiJadwalBimbinganBelajarDesa", ...b })),
|
||||||
|
...tempatKegiatan.map((b) => ({ type: "tempatKegiatan", ...b })),
|
||||||
|
...jenisProgramYangDiselenggarakan.map((b) => ({ type: "jenisProgramYangDiselenggarakan", ...b })),
|
||||||
|
...dataPerpustakaan.map((b) => ({ type: "dataPerpustakaan", ...b })),
|
||||||
|
...dataPendidikan.map((b) => ({ type: "dataPendidikan", ...b })),
|
||||||
|
|
||||||
],
|
],
|
||||||
nextPage: null, // bisa dibuat lebih kompleks kalau perlu
|
nextPage: null, // bisa dibuat lebih kompleks kalau perlu
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const searchState = proxy({
|
|||||||
|
|
||||||
searchState.loading = true;
|
searchState.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await ApiFetch.api.search.findMany.get({
|
const res = await ApiFetch.api.search.findMany.get({
|
||||||
query: {
|
query: {
|
||||||
query: searchState.query,
|
query: searchState.query,
|
||||||
@@ -37,14 +38,27 @@ const searchState = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
if (searchState.page === 1) {
|
||||||
searchState.results = res.data?.data || [];
|
searchState.results = parsedItems;
|
||||||
} else {
|
} else {
|
||||||
searchState.results.push(...(res.data?.data || []));
|
searchState.results.push(...parsedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Search results render:", searchState.results);
|
||||||
|
|
||||||
|
|
||||||
searchState.nextPage = res.data?.nextPage || null;
|
searchState.nextPage = res.data?.nextPage || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search fetch error:", error);
|
||||||
|
} finally {
|
||||||
searchState.loading = false;
|
searchState.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async next() {
|
async next() {
|
||||||
|
|||||||
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,7 +66,19 @@ function Page() {
|
|||||||
</Container>
|
</Container>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={"xs"}>
|
<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>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -22,21 +22,19 @@ interface FileItem {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
const limit = 9; // ✅ ambil 12 data per page
|
||||||
|
|
||||||
// Handle search and pagination changes
|
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
|
||||||
const loadData = useCallback((pageNum: number, searchTerm: string) => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Using the load function from the component's scope
|
|
||||||
const loadFn = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await ApiFetch.api.fileStorage.findMany.get({
|
const query: Record<string, string> = {
|
||||||
query: {
|
|
||||||
category: 'image',
|
category: 'image',
|
||||||
page: pageNum.toString(),
|
page: pageNum.toString(),
|
||||||
limit: '10',
|
limit: limit.toString(),
|
||||||
...(searchTerm && { search: searchTerm })
|
};
|
||||||
}
|
if (searchTerm) query.search = searchTerm;
|
||||||
});
|
|
||||||
|
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
|
||||||
|
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
setFiles(response.data.data || []);
|
setFiles(response.data.data || []);
|
||||||
@@ -50,78 +48,41 @@ interface FileItem {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
loadFn();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initial load and URL change handler
|
// ✅ Initial load + update when URL/search changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = () => {
|
const handleRouteChange = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const urlSearch = urlParams.get('search') || '';
|
const urlSearch = urlParams.get('search') || '';
|
||||||
const urlPage = parseInt(urlParams.get('page') || '1');
|
const urlPage = parseInt(urlParams.get('page') || '1');
|
||||||
|
|
||||||
setSearch(urlSearch);
|
setSearch(urlSearch);
|
||||||
setPage(urlPage);
|
setPage(urlPage);
|
||||||
loadData(urlPage, urlSearch);
|
loadData(urlPage, urlSearch);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle search updates from the search bar
|
|
||||||
const handleSearchUpdate = (e: Event) => {
|
const handleSearchUpdate = (e: Event) => {
|
||||||
const { search } = (e as CustomEvent).detail;
|
const { search } = (e as CustomEvent).detail;
|
||||||
|
|
||||||
setSearch(search);
|
setSearch(search);
|
||||||
setPage(1); // Reset to first page on new search
|
setPage(1);
|
||||||
loadData(1, search);
|
loadData(1, search);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial load
|
|
||||||
handleRouteChange();
|
handleRouteChange();
|
||||||
|
|
||||||
// Set up event listeners
|
|
||||||
window.addEventListener('popstate', handleRouteChange);
|
window.addEventListener('popstate', handleRouteChange);
|
||||||
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('popstate', handleRouteChange);
|
window.removeEventListener('popstate', handleRouteChange);
|
||||||
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
||||||
};
|
};
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
// ✅ Fetch data
|
// ✅ Update when page/search changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchFiles = async () => {
|
loadData(page, search);
|
||||||
setLoading(true);
|
}, [page, search, loadData]);
|
||||||
try {
|
|
||||||
const query: Record<string, string> = {
|
|
||||||
category: 'image',
|
|
||||||
page: page.toString(),
|
|
||||||
limit: '10',
|
|
||||||
};
|
|
||||||
if (search) query.search = search;
|
|
||||||
|
|
||||||
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 updateURL = (newSearch: string, newPage: number) => {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
if (newSearch) url.searchParams.set('search', newSearch);
|
if (newSearch) url.searchParams.set('search', newSearch);
|
||||||
@@ -148,7 +109,14 @@ interface FileItem {
|
|||||||
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||||
{files.map((file) => (
|
{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' }}>
|
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
|
||||||
<Image
|
<Image
|
||||||
src={file.link}
|
src={file.link}
|
||||||
@@ -159,7 +127,6 @@ interface FileItem {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
<Stack gap="sm" py={10}>
|
<Stack gap="sm" py={10}>
|
||||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
||||||
{file.realName || file.name}
|
{file.realName || file.name}
|
||||||
@@ -172,7 +139,6 @@ interface FileItem {
|
|||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -146,24 +146,24 @@ function Page() {
|
|||||||
<Title order={3}>Ajukan Permohonan</Title>
|
<Title order={3}>Ajukan Permohonan</Title>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fz="sm" fw="bold">Nama</Text>}
|
label={<Text fz="sm" fw="bold">Nama</Text>}
|
||||||
placeholder="masukkan nama"
|
placeholder="Masukkan nama"
|
||||||
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
|
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
label={<Text fz="sm" fw="bold">NIK</Text>}
|
label={<Text fz="sm" fw="bold">NIK</Text>}
|
||||||
placeholder="masukkan NIK"
|
placeholder="Masukkan NIK"
|
||||||
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
|
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fz="sm" fw="bold">Alamat</Text>}
|
label={<Text fz="sm" fw="bold">Alamat</Text>}
|
||||||
placeholder="masukkan alamat"
|
placeholder="Masukkan alamat"
|
||||||
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
|
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
|
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)}
|
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
@@ -186,12 +186,11 @@ function Page() {
|
|||||||
stateCreate.create.form.kategoriId = '';
|
stateCreate.create.form.kategoriId = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
searchable
|
// searchable
|
||||||
clearable
|
clearable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,25 +7,39 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function PelayananPerizinanBerusaha() {
|
function PelayananPerizinanBerusaha() {
|
||||||
const state = useProxy(stateLayananDesa)
|
const state = useProxy(stateLayananDesa);
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
const [active, setActive] = useState(1);
|
const [active, setActive] = useState(0);
|
||||||
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
|
|
||||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
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(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await state.pelayananPerizinanBerusaha.findById.load('edit')
|
await state.pelayananPerizinanBerusaha.findById.load('edit');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gagal memuat data:', error);
|
console.error('Gagal memuat data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
loadData()
|
loadData();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const data = state.pelayananPerizinanBerusaha.findById.data;
|
const data = state.pelayananPerizinanBerusaha.findById.data;
|
||||||
|
|
||||||
@@ -33,8 +47,12 @@ function PelayananPerizinanBerusaha() {
|
|||||||
return (
|
return (
|
||||||
<Center mih={300}>
|
<Center mih={300}>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Text fz="lg" fw={500} c="dimmed">Belum ada informasi layanan yang tersedia</Text>
|
<Text fz="lg" fw={500} c="dimmed">
|
||||||
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">Kunjungi OSS</Button>
|
Belum ada informasi layanan yang tersedia
|
||||||
|
</Text>
|
||||||
|
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
|
||||||
|
Kunjungi OSS
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
@@ -57,11 +75,27 @@ function PelayananPerizinanBerusaha() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</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>
|
<Box>
|
||||||
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>Alur pendaftaran NIB:</Text>
|
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
|
||||||
<Stepper active={active} onStepClick={setActive} orientation="vertical" color="blue" radius="md"
|
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={{
|
styles={{
|
||||||
step: { padding: '14px 0' },
|
step: { padding: '14px 0' },
|
||||||
stepBody: { marginLeft: 8 }
|
stepBody: { marginLeft: 8 }
|
||||||
@@ -95,19 +129,42 @@ function PelayananPerizinanBerusaha() {
|
|||||||
</StepperCompleted>
|
</StepperCompleted>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
|
||||||
|
{active < totalSteps && (
|
||||||
<Group justify="center" mt="lg">
|
<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
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
<Button rightSection={<IconArrowRight size={18} />} onClick={nextStep}>
|
|
||||||
Lanjut
|
{active < totalSteps ? (
|
||||||
|
<Button
|
||||||
|
rightSection={active < totalSteps - 1 ? <IconArrowRight size={18} /> : null}
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
{active === totalSteps - 1 ? 'Selesai' : 'Lanjut'}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() => setActive(0)}
|
||||||
|
>
|
||||||
|
Mulai Lagi
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Text fz="sm" ta="justify" c="dimmed" mt="md">
|
<Text fz="sm" ta="justify" c="dimmed" mt="md">
|
||||||
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "}
|
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.
|
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
|
||||||
|
oss.go.id
|
||||||
|
</a>{' '}
|
||||||
|
atau hubungi instansi pemerintah terkait.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
|||||||
<Box pb="xl">
|
<Box pb="xl">
|
||||||
<Group justify="space-between" align="center" mb="md">
|
<Group justify="space-between" align="center" mb="md">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconFileDescription size={28} stroke={1.8} color={colors["blue-button"]} />
|
<IconFileDescription size={28} stroke={1.8} />
|
||||||
<Text fz={{ base: "h4", md: "h2" }} fw={700}>
|
<Text fz={{ base: "h4", md: "h2" }} fw={700}>
|
||||||
Layanan Surat Keterangan
|
Layanan Surat Keterangan
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
|
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
|
||||||
<IconInfoCircle size={22} stroke={1.8} color={colors["blue-button"]} />
|
<IconInfoCircle size={22} stroke={1.8} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function Page() {
|
|||||||
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}>
|
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}>
|
||||||
Potensi Desa Darmasaba
|
Potensi Desa Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="lg" c="dimmed" ta="justify">
|
<Text fz="lg" ta="justify">
|
||||||
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
|
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'
|
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
|
|
||||||
@@ -58,7 +58,6 @@ function LambangDesa() {
|
|||||||
borderColor: '#e0e9ff',
|
borderColor: '#e0e9ff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip label="Deskripsi lambang desa" position="top-start" withArrow>
|
|
||||||
<Text
|
<Text
|
||||||
fz={{ base: 'md', md: 'lg' }}
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
lh={1.8}
|
lh={1.8}
|
||||||
@@ -67,7 +66,6 @@ function LambangDesa() {
|
|||||||
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }}
|
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }}
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core';
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
|
||||||
|
import { IconPhoto } from '@tabler/icons-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { IconPhoto } from '@tabler/icons-react';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
|
|
||||||
function MaskotDesa() {
|
function MaskotDesa() {
|
||||||
const state = useProxy(stateProfileDesa.maskotDesa);
|
const state = useProxy(stateProfileDesa.maskotDesa);
|
||||||
@@ -54,8 +54,8 @@ function MaskotDesa() {
|
|||||||
<Group justify="center" gap="lg" mt="lg">
|
<Group justify="center" gap="lg" mt="lg">
|
||||||
{data.images.length > 0 ? (
|
{data.images.length > 0 ? (
|
||||||
data.images.map((img, index) => (
|
data.images.map((img, index) => (
|
||||||
<Tooltip key={index} label={img.label} position="bottom" withArrow>
|
|
||||||
<Card
|
<Card
|
||||||
|
key={index}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
shadow="md"
|
shadow="md"
|
||||||
withBorder
|
withBorder
|
||||||
@@ -79,7 +79,6 @@ function MaskotDesa() {
|
|||||||
{img.label}
|
{img.label}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
</Tooltip>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Stack align="center" gap="xs" mt="lg">
|
<Stack align="center" gap="xs" mt="lg">
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Divider, Tooltip } from '@mantine/core';
|
import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
|
import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { IconUser, IconBriefcase, IconUsers, IconTargetArrow } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function ProfilPerbekel() {
|
function ProfilPerbekel() {
|
||||||
const state = useProxy(stateProfileDesa.profilPerbekel)
|
const state = useProxy(stateProfileDesa.profilPerbekel)
|
||||||
@@ -93,7 +93,6 @@ function ProfilPerbekel() {
|
|||||||
>
|
>
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Box>
|
<Box>
|
||||||
<Tooltip label="Informasi pribadi perbekel" withArrow>
|
|
||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
<Stack align="center" gap={6}>
|
<Stack align="center" gap={6}>
|
||||||
<IconUser size={22} color={colors['blue-button']} />
|
<IconUser size={22} color={colors['blue-button']} />
|
||||||
@@ -107,11 +106,9 @@ function ProfilPerbekel() {
|
|||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Tooltip label="Pengalaman kerja perbekel" withArrow>
|
|
||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
<Stack align="center" gap={6}>
|
<Stack align="center" gap={6}>
|
||||||
<IconBriefcase size={22} color={colors['blue-button']} />
|
<IconBriefcase size={22} color={colors['blue-button']} />
|
||||||
@@ -125,7 +122,6 @@ function ProfilPerbekel() {
|
|||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client'
|
'use client'
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconUser } from '@tabler/icons-react';
|
import { IconUser } from '@tabler/icons-react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -77,23 +77,17 @@ function SemuaPerbekel() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack gap={4} align="center">
|
<Stack gap={4} align="center">
|
||||||
<Tooltip label="Nama Perbekel" withArrow>
|
|
||||||
<Text fw={700} fz="lg" ta="center">
|
<Text fw={700} fz="lg" ta="center">
|
||||||
{v.nama}
|
{v.nama}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Wilayah menjabat" withArrow>
|
|
||||||
<Text c="dimmed" fz="sm" ta="center">
|
<Text c="dimmed" fz="sm" ta="center">
|
||||||
{v.daerah}
|
{v.daerah}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Periode jabatan" withArrow>
|
|
||||||
<Text c="blue" fw={600} fz="sm" ta="center">
|
<Text c="blue" fw={600} fz="sm" ta="center">
|
||||||
{v.periode}
|
{v.periode}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -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'
|
'use client'
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
@@ -210,28 +46,37 @@ function Page() {
|
|||||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
<Title order={3}>Pendapatan</Title>
|
<Title order={3}>Pendapatan</Title>
|
||||||
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
|
{latestApb?.pendapatan?.map((item) => (
|
||||||
<Box key={item.id}>
|
<Box key={item.id}>
|
||||||
<Grid>
|
<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>
|
<Text fz="md" fw={500}>{item.name}</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
<Text
|
||||||
style: 'currency',
|
fz="md"
|
||||||
currency: 'IDR',
|
fw={500}
|
||||||
minimumFractionDigits: 0
|
style={{
|
||||||
}).format(item.value)}</Text>
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
textAlign: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
|
||||||
|
</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<Grid>
|
<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>
|
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
<Text fz="xl" fw={700} c={colors['blue-button']}>
|
<Text style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal'
|
||||||
|
}} fz="xl" fw={700} c={colors['blue-button']}>
|
||||||
{new Intl.NumberFormat('id-ID', {
|
{new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
@@ -247,18 +92,28 @@ function Page() {
|
|||||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
<Title order={3}>Belanja</Title>
|
<Title order={3}>Belanja</Title>
|
||||||
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
|
{latestApb?.belanja?.map((item) => (
|
||||||
<Box key={item.id}>
|
<Box key={item.id}>
|
||||||
<Grid>
|
<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>
|
<Text fz="md" fw={500}>{item.name}</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
<Text
|
||||||
|
fz="md"
|
||||||
|
fw={500}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
textAlign: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
minimumFractionDigits: 0
|
minimumFractionDigits: 0
|
||||||
}).format(item.value)}</Text>
|
}).format(item.value)}
|
||||||
|
</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -284,18 +139,28 @@ function Page() {
|
|||||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
<Title order={3}>Pembiayaan</Title>
|
<Title order={3}>Pembiayaan</Title>
|
||||||
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
|
{latestApb?.pembiayaan?.map((item) => (
|
||||||
<Box key={item.id}>
|
<Box key={item.id}>
|
||||||
<Grid>
|
<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>
|
<Text fz="md" fw={500}>{item.name}</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
<Text
|
||||||
|
fz="md"
|
||||||
|
fw={500}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
textAlign: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
minimumFractionDigits: 0
|
minimumFractionDigits: 0
|
||||||
}).format(item.value)}</Text>
|
}).format(item.value)}
|
||||||
|
</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -367,4 +232,3 @@ function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function Page() {
|
|||||||
p={10}
|
p={10}
|
||||||
mb={50}
|
mb={50}
|
||||||
h={400}
|
h={400}
|
||||||
w={150}
|
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
|
||||||
data={data.map((item) => ({
|
data={data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
Pekerjaan: item.pekerjaan,
|
Pekerjaan: item.pekerjaan,
|
||||||
|
|||||||
@@ -72,21 +72,21 @@ function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</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"}>
|
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||||
Jumlah Penduduk Usia Kerja Yang Menganggur
|
Jumlah Penduduk Usia Kerja Yang Menganggur
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 50, lg: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
<Paper p={'lg'}>
|
<Paper p={'lg'}>
|
||||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
|
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
|
||||||
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
{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
|
<PieChart
|
||||||
w="100%"
|
w="100%"
|
||||||
h={250} // lebih kecil biar aman di mobile
|
h={250} // lebih kecil biar aman di mobile
|
||||||
@@ -133,7 +133,7 @@ function Page() {
|
|||||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||||
<PieChart
|
<PieChart
|
||||||
w="100%"
|
w="100%"
|
||||||
h={250} // lebih kecil biar aman di mobile
|
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
|
||||||
withLabelsLine
|
withLabelsLine
|
||||||
labelsPosition="outside"
|
labelsPosition="outside"
|
||||||
labelsType="percent"
|
labelsType="percent"
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ function Page() {
|
|||||||
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
|
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
|
||||||
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
|
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
|
||||||
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
|
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
|
||||||
<TableTd ta={'center'}>{item.percentageChange}</TableTd>
|
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function DetailLowonganKerjaUser() {
|
||||||
|
const state = useProxy(lowonganKerjaState);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
await state.findUnique.load(params?.id as string);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const data = state.findUnique.data;
|
||||||
|
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<Center py="xl">
|
||||||
|
<Skeleton height={500} w={{ base: '90%', md: '70%' }} radius="lg" />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
|
||||||
|
<Box w={{ base: '100%', md: '70%' }}>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconArrowBack size={20} />}
|
||||||
|
mb="md"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
radius="lg"
|
||||||
|
shadow="md"
|
||||||
|
withBorder
|
||||||
|
p="xl"
|
||||||
|
bg={colors['white-1']}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Judul */}
|
||||||
|
<Text fz={{ base: '1.6rem', md: '2rem' }} fw={700} c={colors['blue-button']}>
|
||||||
|
{data.posisi}
|
||||||
|
</Text>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Info Ringkas */}
|
||||||
|
<Stack gap="sm" mt="md">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconBriefcase size={20} color={colors['blue-button']} />
|
||||||
|
<Text fz="md" fw={600}>{data.namaPerusahaan}</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} color={colors['blue-button']} />
|
||||||
|
<Text fz="md">{data.lokasi}</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconPhone size={20} color={colors['blue-button']} />
|
||||||
|
<Text fz="md">{data.notelp}</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconCurrencyDollar size={20} color={colors['blue-button']} />
|
||||||
|
<Text fz="md">{data.gaji || '-'}</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconBriefcase size={20} color={colors['blue-button']} />
|
||||||
|
<Text fz="md">{data.tipePekerjaan}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw={600} fz="lg" mb={4}>
|
||||||
|
Deskripsi Pekerjaan
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
lh={1.6}
|
||||||
|
style={{ wordBreak: 'break-word' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw={600} fz="lg" mb={4}>
|
||||||
|
Kualifikasi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
lh={1.6}
|
||||||
|
style={{ wordBreak: 'break-word' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Center>
|
||||||
|
<Button
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
mt="md"
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
onClick={() => window.open(`https://wa.me/${data.notelp}`, '_blank')}
|
||||||
|
leftSection={<IconBrandWhatsapp size={20} />}
|
||||||
|
>
|
||||||
|
Hubungi Sekarang
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailLowonganKerjaUser;
|
||||||
@@ -103,7 +103,7 @@ function Page() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Button onClick={() => router.push(`https://wa.me/${v.notelp?.replace(/\D/g, '')}`)}>Lamar Sekarang</Button>
|
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)}>Detail</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
|
|||||||
157
src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx
Normal file
157
src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
'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 */}
|
||||||
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali ke daftar produk
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<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={250}
|
||||||
|
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;
|
||||||
@@ -71,8 +71,11 @@ function Page() {
|
|||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} >
|
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||||
Pasar Desa Online merupakan Media Promosi yang bertujuan untuk membantu warga desa dalam memasarkan dan memperkenalkan produknya kepada masyarakat.
|
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan
|
||||||
|
</Text>
|
||||||
|
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||||
|
dan memperkenalkan produk mereka.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
@@ -105,7 +108,7 @@ function Page() {
|
|||||||
return (
|
return (
|
||||||
<Stack key={k}>
|
<Stack key={k}>
|
||||||
<motion.div
|
<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 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.8 }}
|
whileTap={{ scale: 0.8 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
@@ -49,23 +75,30 @@ function Page() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Paper p={'xl'}>
|
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
||||||
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text>
|
<Paper p="xl">
|
||||||
|
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
|
||||||
|
<Box style={{ width: '100%', minWidth: '600px' }}>
|
||||||
<BarChart
|
<BarChart
|
||||||
p={10}
|
p={10}
|
||||||
h={300}
|
h={300}
|
||||||
data={data.map((item) => ({
|
data={chartData}
|
||||||
id: item.id,
|
|
||||||
sektor: item.name,
|
|
||||||
Ton: item.value,
|
|
||||||
}))}
|
|
||||||
dataKey="sektor"
|
dataKey="sektor"
|
||||||
series={[
|
series={[
|
||||||
{ name: 'Ton', color: colors['blue-button'] },
|
{ name: 'Ton', color: colors['blue-button'] },
|
||||||
]}
|
]}
|
||||||
tickLine="y"
|
tickLine="y"
|
||||||
|
tooltipAnimationDuration={200}
|
||||||
|
withTooltip
|
||||||
|
style={{
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
xAxisLabel="Sektor"
|
||||||
|
yAxisLabel="Ton"
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ function Page() {
|
|||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Text fz={'h4'}>Mewujudkan Desa Darmasaba sebagai pusat inovasi digital yang memberdayakan masyarakat, meningkatkan kesejahteraan, dan menciptakan peluang ekonomi berbasis teknologi.</Text>
|
<Text fz={'md'}>Menjadikan Desa Darmasaba pusat inovasi digital untuk pemberdayaan masyarakat</Text>
|
||||||
|
<Text fz={'md'}>dan peningkatan ekonomi berbasis teknologi.</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
|
||||||
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
|
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
|
||||||
import colors from '@/con/colors';
|
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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconSearch } from '@tabler/icons-react';
|
||||||
import { useTransitionRouter } from 'next-view-transitions';
|
import { useTransitionRouter } from 'next-view-transitions';
|
||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
import { IconSearch } from '@tabler/icons-react';
|
|
||||||
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
|
|
||||||
|
|
||||||
// const data = [
|
// const data = [
|
||||||
// {
|
// {
|
||||||
@@ -75,17 +75,23 @@ function Page() {
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} >
|
<Box px={{ base: 'md', md: 100 }} >
|
||||||
<Group justify="space-between" mb="md" align='center'>
|
<Grid align='center'>
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<GridCol span={{ base: 12, md: 9 }}>
|
||||||
|
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||||
Program Kreatif Desa
|
Program Kreatif Desa
|
||||||
</Text>
|
</Text>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, md: 3 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Cari program kreatif..."
|
radius={"lg"}
|
||||||
leftSection={<IconSearch size={20} />}
|
placeholder='Cari Program Kreatif'
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftSection={<IconSearch size={20} />}
|
||||||
|
w={{ base: "50%", md: "100%" }}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ function Page() {
|
|||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} >
|
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" mt={4} >
|
||||||
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
|
|||||||
@@ -50,10 +50,12 @@ function Page() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder='Cari kontak darurat, nama, atau nomor...'
|
radius={"lg"}
|
||||||
leftSection={<IconSearch size={20} />}
|
placeholder='Cari Kontak Darurat'
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftSection={<IconSearch size={20} />}
|
||||||
|
w={{ base: "100%", md: "25%" }}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
@@ -95,10 +97,12 @@ function Page() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder='Cari kontak darurat, nama, atau nomor...'
|
radius={"lg"}
|
||||||
leftSection={<IconSearch size={20} />}
|
placeholder='Cari Kontak Darurat'
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftSection={<IconSearch size={20} />}
|
||||||
|
w={{ base: "50%", md: "100%" }}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
|
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
|
||||||
import colors from '@/con/colors';
|
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 { DateTimePicker } from '@mantine/dates';
|
||||||
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
|
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 { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
import { useTransitionRouter } from 'next-view-transitions';
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -53,14 +53,17 @@ function Page() {
|
|||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Flex justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Cari laporan"
|
radius={"lg"}
|
||||||
|
placeholder='Cari Laporan Publik'
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftSection={<IconSearch size={20} />}
|
||||||
|
w={{ base: "100%", md: "30%" }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
@@ -115,7 +118,7 @@ function Page() {
|
|||||||
return (
|
return (
|
||||||
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
|
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
|
||||||
<Stack>
|
<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'}>
|
<Text fs={'italic'} fz={'xl'}>
|
||||||
{v.tanggalWaktu
|
{v.tanggalWaktu
|
||||||
? new Date(v.tanggalWaktu).toLocaleString('id-ID')
|
? new Date(v.tanggalWaktu).toLocaleString('id-ID')
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function DetailPencegahanKriminalitas() {
|
|||||||
const data = kriminalitasState.findUnique.data;
|
const data = kriminalitasState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py="md" px="md">
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|||||||
@@ -41,13 +41,14 @@ function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||||
Pencegahan Kriminalitas
|
Pencegahan Kriminalitas
|
||||||
</Text>
|
</Text>
|
||||||
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
|
<Text fz='md'>
|
||||||
Keamanan Komunitas & Pencegahan Kriminal
|
Keamanan Komunitas & Pencegahan Kriminal
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -61,31 +62,93 @@ function Page() {
|
|||||||
Program Keamanan Berjalan
|
Program Keamanan Berjalan
|
||||||
</Text>
|
</Text>
|
||||||
<Stack pt={30} gap="lg">
|
<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 }}>
|
||||||
|
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||||
|
Pencegahan Kriminalitas
|
||||||
|
</Text>
|
||||||
|
<Text fz='md'>
|
||||||
|
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">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
minHeight: 300, // sesuaikan: tinggi area yg muat 3 item
|
||||||
|
}}
|
||||||
|
>
|
||||||
{data.length > 0 ? (
|
{data.length > 0 ? (
|
||||||
data.map((item) => (
|
data.map((item) => (
|
||||||
<a key={item.id} href={`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`}>
|
<Paper
|
||||||
<Paper p="md" bg={colors['blue-button']} radius="md" shadow="sm">
|
key={item.id}
|
||||||
<Stack gap={"xs"}>
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: colors['blue-button'],
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)
|
||||||
|
}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.backgroundColor = '#1a3e7a')
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.backgroundColor = colors['blue-button'])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
<Text fz="h3" c={colors['white-1']}>
|
<Text fz="h3" c={colors['white-1']}>
|
||||||
{item.judul}
|
{item.judul}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</a>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text color="dimmed">
|
<Text c="dimmed">Tidak ada data pencegahan kriminalitas yang cocok</Text>
|
||||||
Tidak ada data pencegahan kriminalitas yang cocok
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
<Button
|
<Button
|
||||||
mt={20}
|
mt={20}
|
||||||
fullWidth
|
fullWidth
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="md"
|
size="md"
|
||||||
bg={colors['blue-button']}
|
variant="outline"
|
||||||
rightSection={<IconArrowRight size={20} color={colors['white-1']} />}
|
color="blue"
|
||||||
onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`)}
|
rightSection={<IconArrowRight size={20} />}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
fontWeight: 600,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Jelajahi Program Lainnya
|
Jelajahi Program Lainnya
|
||||||
</Button>
|
</Button>
|
||||||
@@ -111,9 +174,7 @@ function Page() {
|
|||||||
<Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}>
|
<Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}>
|
||||||
{findFirst.data?.judul}
|
{findFirst.data?.judul}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="h4" c={colors['blue-button']}>
|
<Text fz="h4" dangerouslySetInnerHTML={{ __html: findFirst.data?.deskripsiSingkat }} />
|
||||||
{findFirst.data?.deskripsiSingkat}
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -21,12 +21,23 @@ import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan
|
|||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
||||||
|
import { IconArrowLeft } from '@tabler/icons-react';
|
||||||
|
|
||||||
function PencegahanKriminalitas() {
|
function PencegahanKriminalitas() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowLeft size={20} />}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title="Program Pencegahan Kriminalitas"
|
title="Program Pencegahan Kriminalitas"
|
||||||
placeholder="Cari program atau deskripsi..."
|
placeholder="Cari program atau deskripsi..."
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function Page() {
|
|||||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||||
Kantor Polisi Terdekat
|
Kantor Polisi Terdekat
|
||||||
</Text>
|
</Text>
|
||||||
<Text pb={15} fz={'h4'} >
|
<Text pb={15} fz={'md'} >
|
||||||
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
|
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -78,15 +78,12 @@ function Page() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Text fz="h4" fw="bold">Pendahuluan</Text>
|
<Text fz="h4" fw="bold">Pendahuluan</Text>
|
||||||
<Divider my="xs" />
|
<Divider my="xs" />
|
||||||
<Text fz="md" lh={1.6} ta="justify">
|
<Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
|
||||||
{state.findUnique.data.introduction?.content}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<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" />
|
<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 }} />
|
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
|
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Anchor, Box, Card, Divider, Group, Image, Loader, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCalendar, IconChevronRight } from '@tabler/icons-react';
|
import { IconCalendar, IconChevronRight } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -28,9 +28,9 @@ function ArtikelKesehatanPage() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md">
|
<Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={2} ta="center" c={colors['blue-button']}>
|
<Text ta="center" fw={700} fz="32px" c={colors['blue-button']}>
|
||||||
Artikel Kesehatan
|
Artikel Kesehatan
|
||||||
</Title>
|
</Text>
|
||||||
<Divider size="sm" color={colors['blue-button']} />
|
<Divider size="sm" color={colors['blue-button']} />
|
||||||
{state.findMany.data.length === 0 ? (
|
{state.findMany.data.length === 0 ? (
|
||||||
<Box py="xl" ta="center">
|
<Box py="xl" ta="center">
|
||||||
@@ -51,7 +51,7 @@ function ArtikelKesehatanPage() {
|
|||||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||||
>
|
>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy"/>
|
<Image style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }} src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy" />
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Stack gap="xs" mt="md">
|
<Stack gap="xs" mt="md">
|
||||||
<Text fw="bold" fz="xl" c="dark">{item.title}</Text>
|
<Text fw="bold" fz="xl" c="dark">{item.title}</Text>
|
||||||
@@ -64,18 +64,17 @@ function ArtikelKesehatanPage() {
|
|||||||
<Text fz="md" c="dark" lineClamp={3}>
|
<Text fz="md" c="dark" lineClamp={3}>
|
||||||
{item.content}
|
{item.content}
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip label="Baca artikel lengkap">
|
<Group justify="flex-start">
|
||||||
<Anchor
|
<Button
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="lg"
|
||||||
|
size="sm"
|
||||||
|
rightSection={<IconChevronRight size={18} />}
|
||||||
onClick={() => router.push(`/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/${item.id}`)}
|
onClick={() => router.push(`/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/${item.id}`)}
|
||||||
variant="light"
|
|
||||||
c={colors['blue-button']}
|
|
||||||
>
|
>
|
||||||
<Group gap="xs">
|
Baca Selengkapnya
|
||||||
<Text fw="bold" fz="md">Baca Selengkapnya</Text>
|
</Button>
|
||||||
<IconChevronRight size={18} />
|
|
||||||
</Group>
|
</Group>
|
||||||
</Anchor>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useParams } from 'next/navigation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -149,11 +149,6 @@ function Page() {
|
|||||||
</CopyButton>
|
</CopyButton>
|
||||||
</Group>
|
</Group>
|
||||||
</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>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</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={<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>
|
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Anchor target="_blank" underline="hover">Kunjungi situs resmi</Anchor>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -246,14 +240,7 @@ function Page() {
|
|||||||
</Table>
|
</Table>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</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">
|
<Stack gap="md">
|
||||||
<Title order={3}>Fasilitas Pendukung</Title>
|
<Title order={3}>Fasilitas Pendukung</Title>
|
||||||
@@ -270,8 +257,7 @@ function Page() {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
|
||||||
<Card radius="xl" p="lg" withBorder>
|
<Card radius="xl" p="lg" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Layanan & Tarif</Title>
|
<Title order={3}>Layanan & Tarif</Title>
|
||||||
@@ -309,6 +295,7 @@ function Page() {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Anchor, Badge, Box, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { IconMapPin, IconClock, IconArrowRight } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function FasilitasKesehatanPage() {
|
function FasilitasKesehatanPage() {
|
||||||
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
|
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
|
||||||
@@ -84,20 +84,21 @@ function FasilitasKesehatanPage() {
|
|||||||
{item.informasiumum.jamOperasional}
|
{item.informasiumum.jamOperasional}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Anchor
|
<Group justify="flex-start">
|
||||||
|
<Button
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
radius="lg"
|
||||||
|
size="sm"
|
||||||
|
rightSection={<IconChevronRight size={18} />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}`
|
`/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
c={colors['blue-button']}
|
|
||||||
fz="sm"
|
|
||||||
fw={600}
|
|
||||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
|
||||||
>
|
>
|
||||||
Lihat Detail
|
Lihat Detail
|
||||||
<IconArrowRight size={16} stroke={1.5} />
|
</Button>
|
||||||
</Anchor>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text
|
Text
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
|
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page';
|
|||||||
function Page() {
|
function Page() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const state = useProxy(jadwalkegiatanState);
|
const state = useProxy(jadwalkegiatanState);
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
state.findUnique.load(params?.id as string);
|
state.findUnique.load(params?.id as string);
|
||||||
@@ -87,7 +90,17 @@ function Page() {
|
|||||||
<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>
|
</Stack>
|
||||||
|
|
||||||
|
<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 />
|
<CreatePendaftaran />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
|
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ function JadwalKegiatanPage() {
|
|||||||
|
|
||||||
<Divider my="sm" />
|
<Divider my="sm" />
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-start">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
bg={colors['blue-button']}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="sm"
|
size="sm"
|
||||||
rightSection={<IconChevronRight size={18} />}
|
rightSection={<IconChevronRight size={18} />}
|
||||||
@@ -84,14 +84,6 @@ function JadwalKegiatanPage() {
|
|||||||
`/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/${item.id}`
|
`/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/${item.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
background: colors['blue-button'],
|
|
||||||
color: 'white',
|
|
||||||
boxShadow: '0 0 12px rgba(0, 123, 255, 0.4)',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Lihat Detail & Daftar
|
Lihat Detail & Daftar
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ function DetailInfoWabahPenyakitUser() {
|
|||||||
const data = state.findUnique.data;
|
const data = state.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10} px={{ base: 'md', md: 100 }}>
|
||||||
{/* Tombol Back */}
|
{/* Tombol Back */}
|
||||||
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -38,6 +39,7 @@ function DetailInfoWabahPenyakitUser() {
|
|||||||
>
|
>
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Wrapper Detail */}
|
{/* Wrapper Detail */}
|
||||||
<Paper
|
<Paper
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Informasi Wabah & Penyakit
|
Informasi Wabah & Penyakit
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="md" c="dimmed" mt={4}>
|
<Text fz="md" mt={4}>
|
||||||
Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang
|
Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang
|
||||||
diawasi.
|
diawasi.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -84,7 +84,7 @@ function Page() {
|
|||||||
<Center py="6xl">
|
<Center py="6xl">
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<IconInfoCircle size={50} color={colors['blue-button']} />
|
<IconInfoCircle size={50} color={colors['blue-button']} />
|
||||||
<Text fz="lg" fw={500} c="dimmed">
|
<Text fz="lg" fw={500} >
|
||||||
Tidak ada data yang cocok dengan pencarian Anda.
|
Tidak ada data yang cocok dengan pencarian Anda.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -101,17 +101,35 @@ function Page() {
|
|||||||
bg={colors['white-trans-1']}
|
bg={colors['white-trans-1']}
|
||||||
style={{
|
style={{
|
||||||
transition: 'transform 200ms ease, box-shadow 200ms ease',
|
transition: 'transform 200ms ease, box-shadow 200ms ease',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm" style={{ flex: 1 }}>
|
||||||
<Image
|
{/* Gambar */}
|
||||||
radius="md"
|
<Box
|
||||||
h={180}
|
h={180}
|
||||||
src={v.image.link}
|
w="100%"
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={v.image?.link}
|
||||||
alt={v.name}
|
alt={v.name}
|
||||||
fit="cover"
|
fit="cover"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
style={{
|
||||||
|
objectFit: 'cover',
|
||||||
|
objectPosition: 'center',
|
||||||
|
}}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Judul dan badge */}
|
||||||
<Group justify="space-between" mt="sm">
|
<Group justify="space-between" mt="sm">
|
||||||
<Text fw={700} fz="lg" c={colors['blue-button']}>
|
<Text fw={700} fz="lg" c={colors['blue-button']}>
|
||||||
{v.name}
|
{v.name}
|
||||||
@@ -120,20 +138,46 @@ function Page() {
|
|||||||
Wabah
|
Wabah
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Text fz="sm" c="dimmed">
|
<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>
|
</Text>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
|
|
||||||
{v.deskripsiSingkat}
|
{/* Bagian deskripsi dan tombol */}
|
||||||
</Text>
|
<Box style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
lh={1.5}
|
||||||
|
lineClamp={3}
|
||||||
|
dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }}
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
mt="md"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
Selengkapnya
|
Selengkapnya
|
||||||
</Button>
|
</Button>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -101,15 +101,30 @@ function Page() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '16/9',
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={v.image.link}
|
src={v.image.link}
|
||||||
alt={v.name}
|
alt={v.name}
|
||||||
w={140}
|
fit="cover"
|
||||||
h={140}
|
|
||||||
fit="contain"
|
|
||||||
radius="md"
|
|
||||||
loading="lazy"
|
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']}>
|
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Image, Paper, Skeleton, Stack, Text } 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 DetailPenangananDaruratUser() {
|
||||||
|
const state = useProxy(penangananDarurat);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!state.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={40}>
|
||||||
|
<Skeleton height={400} radius="md" />
|
||||||
|
<Skeleton height={20} width="80%" radius="md" />
|
||||||
|
<Skeleton height={20} width="60%" radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = state.findUnique.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={20}>
|
||||||
|
{/* Tombol Back */}
|
||||||
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
|
||||||
|
mb={20}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Wrapper Detail */}
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
w={{ base: '100%', md: '70%', lg: '60%' }}
|
||||||
|
mx="auto"
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="xl"
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md" align="center" ta="center">
|
||||||
|
<Text fz="xl" fw={700} c={colors['blue-button']}>
|
||||||
|
{data.name || 'Penanganan Darurat'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{data.image?.link && (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.name}
|
||||||
|
radius="md"
|
||||||
|
mah={300}
|
||||||
|
fit="contain"
|
||||||
|
loading="lazy"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
ta="justify"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailPenangananDaruratUser;
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
|
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Grid,
|
Grid,
|
||||||
GridCol,
|
GridCol,
|
||||||
@@ -106,15 +106,34 @@ function Page() {
|
|||||||
>
|
>
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Center>
|
<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
|
<Image
|
||||||
src={v.image.link}
|
src={v.image?.link || '/img/default.png'}
|
||||||
alt={v.name}
|
alt={v.name}
|
||||||
h={180}
|
|
||||||
w="100%"
|
|
||||||
radius="md"
|
|
||||||
fit="cover"
|
fit="cover"
|
||||||
style={{ aspectRatio: '4/3' }}
|
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>
|
</Center>
|
||||||
<Stack gap={4} w="100%">
|
<Stack gap={4} w="100%">
|
||||||
<Text
|
<Text
|
||||||
@@ -130,15 +149,20 @@ function Page() {
|
|||||||
<Text
|
<Text
|
||||||
fz="sm"
|
fz="sm"
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
lineClamp={4}
|
lineClamp={3}
|
||||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Button
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
component="a"
|
||||||
|
href={`/darmasaba/kesehatan/penanganan-darurat/${v.id}`}
|
||||||
|
>
|
||||||
|
Lihat Detail
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Badge radius="md" color="blue" variant="light" mt="sm">
|
|
||||||
Darurat
|
|
||||||
</Badge>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
|
|||||||
121
src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx
Normal file
121
src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'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>
|
||||||
|
|
||||||
|
{/* Konten utama */}
|
||||||
|
<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="center" gap="xs">
|
||||||
|
<IconPhone size={18} stroke={1.5} />
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{data.nomor || 'Nomor tidak tersedia'}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
<IconCalendar size={18} stroke={1.5} />
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
|
||||||
|
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
<IconInfoCircle size={18} stroke={1.5} />
|
||||||
|
<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'
|
'use client'
|
||||||
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
|
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
|
||||||
import colors from "@/con/colors";
|
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 { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
|
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
import BackButton from "../../desa/layanan/_com/BackButto";
|
import BackButton from "../../desa/layanan/_com/BackButto";
|
||||||
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const state = useProxy(posyandustate);
|
const state = useProxy(posyandustate);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||||
|
const router = useTransitionRouter()
|
||||||
|
|
||||||
const { data, page, totalPages, loading, load } = state.findMany;
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 6, search);
|
load(page, 6, debouncedSearch);
|
||||||
}, [page, search]);
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
@@ -131,33 +134,41 @@ export default function Page() {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
<Flex align="center" gap="xs">
|
<Flex align="flex-start" gap="xs">
|
||||||
<IconPhone size={18} stroke={1.5} />
|
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||||
<Text fz="sm" c="dimmed">
|
<Box>
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||||
{v.nomor || "Tidak tersedia"}
|
{v.nomor || "Tidak tersedia"}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex align="center" gap="xs">
|
|
||||||
<IconCalendar size={18} stroke={1.5} />
|
<Flex align="flex-start" gap="xs">
|
||||||
<Text fz="sm" c="dimmed">
|
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||||
Jadwal:{" "}
|
<Box>
|
||||||
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} />
|
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||||
|
<strong>Jadwal:</strong>{" "}
|
||||||
|
<span
|
||||||
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Spoiler
|
|
||||||
key={`spoiler-${v.id}`}
|
<Flex align="flex-start" gap="xs">
|
||||||
maxHeight={70}
|
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||||
showLabel="Lihat selengkapnya"
|
|
||||||
hideLabel="Sembunyikan"
|
|
||||||
transitionDuration={300}
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
fz="sm"
|
fz="sm"
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
|
c="dimmed"
|
||||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
Program Kesehatan Desa
|
Program Kesehatan Desa
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="lg" c="dimmed" mt="xs">
|
<Text fz="lg" mt="xs">
|
||||||
Temukan berbagai program kesehatan untuk mendukung kualitas hidup
|
Temukan berbagai program kesehatan untuk mendukung kualitas hidup
|
||||||
masyarakat Darmasaba.
|
masyarakat Darmasaba.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -126,18 +126,37 @@ export default function Page() {
|
|||||||
className="hover-scale"
|
className="hover-scale"
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Box h={180} w="100%">
|
<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
|
<Image
|
||||||
src={v.image?.link}
|
src={v.image?.link || '/img/default.png'}
|
||||||
alt={v.name}
|
alt={v.name}
|
||||||
radius="xl"
|
|
||||||
w="100%"
|
|
||||||
h="100%"
|
|
||||||
fit="cover"
|
fit="cover"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
loading="lazy"
|
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>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
</Center>
|
||||||
|
|
||||||
<Box px="lg" pb="lg">
|
<Box px="lg" pb="lg">
|
||||||
<Text
|
<Text
|
||||||
fw="bold"
|
fw="bold"
|
||||||
@@ -149,7 +168,7 @@ export default function Page() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fz="sm"
|
fz="sm"
|
||||||
c="dimmed"
|
ta={"justify"}
|
||||||
lineClamp={3}
|
lineClamp={3}
|
||||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
@@ -230,7 +249,7 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
Manfaat Program Kesehatan
|
Manfaat Program Kesehatan
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="lg" c="dimmed" maw={700}>
|
<Text fz="lg" maw={700}>
|
||||||
Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan
|
Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan
|
||||||
kesejahteraan dan kualitas hidup warganya.
|
kesejahteraan dan kualitas hidup warganya.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -260,7 +279,7 @@ export default function Page() {
|
|||||||
<Text ta="center" fw="bold" fz="xl" c={colors["blue-button"]}>
|
<Text ta="center" fw="bold" fz="xl" c={colors["blue-button"]}>
|
||||||
{v.title}
|
{v.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta="center" fz="sm" c="dimmed">
|
<Text ta="center" fz="sm">
|
||||||
{v.desc}
|
{v.desc}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function Page() {
|
|||||||
<Text fz={{ base: "2rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
|
<Text fz={{ base: "2rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
|
||||||
Daftar Puskesmas
|
Daftar Puskesmas
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="md">
|
||||||
Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba
|
Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|||||||
@@ -71,8 +71,11 @@ function Page() {
|
|||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Text fz="lg" c={'black'}>
|
<Text fz="md" >
|
||||||
Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya. Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau.
|
Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya.
|
||||||
|
</Text>
|
||||||
|
<Text fz="md">
|
||||||
|
Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Create a new component: components/EdukasiCard.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Box, Paper, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface EdukasiCardProps {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: EdukasiCardProps) {
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
p={{ base: 'md', md: 'lg' }}
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-4px)',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack h="100%" justify="space-between" gap="md">
|
||||||
|
<Box>
|
||||||
|
<Stack align="center" gap="xs" mb="md">
|
||||||
|
<Box style={{ color }}>{icon}</Box>
|
||||||
|
<Tooltip label={title} maw={250} multiline withArrow position="top">
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'h5', md: 'h4' }}
|
||||||
|
fw={700}
|
||||||
|
c={color}
|
||||||
|
ta="center"
|
||||||
|
lineClamp={2}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
minHeight: '3.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: 'var(--mantine-color-gray-7)'
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,130 +1,104 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
|
|
||||||
import colors from '@/con/colors';
|
import { Box, Container, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
|
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
|
|
||||||
function Page() {
|
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
|
||||||
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById)
|
import colors from '@/con/colors';
|
||||||
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById)
|
import { EdukasiCard } from './component/edukasiCard';
|
||||||
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||||
|
{[1, 2, 3].map((item) => (
|
||||||
|
<Skeleton key={item} height={300} radius="md" />
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EdukasiLingkunganPage() {
|
||||||
|
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById);
|
||||||
|
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById);
|
||||||
|
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
tujuan.load('edit')
|
tujuan.load('edit');
|
||||||
materi.load('edit')
|
materi.load('edit');
|
||||||
contoh.load('edit')
|
contoh.load('edit');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) {
|
const isLoading = tujuan.loading || !tujuan.data ||
|
||||||
|
materi.loading || !materi.data ||
|
||||||
|
contoh.loading || !contoh.data;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Stack py={20}>
|
<Stack py="xl" px={{ base: 'md', md: 'xl' }}>
|
||||||
<Skeleton radius="md" height={600} />
|
<BackButton />
|
||||||
|
<LoadingSkeleton />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
<Stack bg={colors.Bg} py="xl" gap="xl" px={{ base: 'md', md: 'xl' }}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={20}>
|
<Container size="lg" ta="center">
|
||||||
<Text ta={'center'} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
|
<Text
|
||||||
|
component="h1"
|
||||||
|
fz={{ base: 'h2', md: '2.5rem' }}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fw={700}
|
||||||
|
mb="md"
|
||||||
|
>
|
||||||
Edukasi Lingkungan
|
Edukasi Lingkungan
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta={'center'} fz="h4" c="black">
|
<Text
|
||||||
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
|
c="dimmed"
|
||||||
|
maw={800}
|
||||||
|
mx="auto"
|
||||||
|
>
|
||||||
Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam,
|
Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam,
|
||||||
meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama.
|
meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Container>
|
||||||
|
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Container size="xl">
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
|
<SimpleGrid
|
||||||
{/* Tujuan Edukasi Lingkungan */}
|
cols={{ base: 1, sm: 2, lg: 3 }}
|
||||||
<Box style={{ display: 'flex', height: '100%' }}>
|
spacing="xl"
|
||||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
|
verticalSpacing={{ base: 'md', md: 'xl' }}
|
||||||
<Stack gap="md">
|
>
|
||||||
<Box>
|
<EdukasiCard
|
||||||
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
|
icon={<IconLeaf size={32} />}
|
||||||
<Stack gap={4} align="center">
|
title={tujuan.data?.judul || ''}
|
||||||
<IconLeaf size={28} color={colors['blue-button']} />
|
description={tujuan.data?.deskripsi || ''}
|
||||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
color={colors['blue-button']}
|
||||||
{tujuan.data?.judul}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
wordBreak: "break-word",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
flexGrow: 1
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }}
|
|
||||||
/>
|
/>
|
||||||
<Box style={{ flexGrow: 1 }} />
|
|
||||||
</Stack>
|
<EdukasiCard
|
||||||
</Paper>
|
icon={<IconRecycle size={32} />}
|
||||||
</Box>
|
title={materi.data?.judul || ''}
|
||||||
{/* Materi Edukasi Lingkungan */}
|
description={materi.data?.deskripsi || ''}
|
||||||
<Box style={{ display: 'flex', height: '100%' }}>
|
color={colors['blue-button']}
|
||||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
|
||||||
<Stack gap="md">
|
|
||||||
<Box>
|
|
||||||
<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>
|
|
||||||
</Stack>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
wordBreak: "break-word",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
flexGrow: 1
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
|
|
||||||
/>
|
/>
|
||||||
<Box style={{ flexGrow: 1 }} />
|
|
||||||
</Stack>
|
<EdukasiCard
|
||||||
</Paper>
|
icon={<IconPlant2 size={32} />}
|
||||||
</Box>
|
title={contoh.data?.judul || ''}
|
||||||
{/* Contoh Edukasi Lingkungan */}
|
description={contoh.data?.deskripsi || ''}
|
||||||
<Box style={{ display: 'flex', height: '100%' }}>
|
color={colors['blue-button']}
|
||||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
|
||||||
<Stack gap="md">
|
|
||||||
<Box>
|
|
||||||
<Tooltip label={contoh.data?.judul} position="top" withArrow>
|
|
||||||
<Stack gap={4} align="center">
|
|
||||||
<IconPlant2 size={28} color={colors['blue-button']} />
|
|
||||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
|
||||||
{contoh.data?.judul}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
wordBreak: "break-word",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
flexGrow: 1
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Container>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'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 gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -23,12 +27,11 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||||
import { useTransitionRouter } from 'next-view-transitions';
|
import { useTransitionRouter } from 'next-view-transitions';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
|
|
||||||
export default function Content({ kategori }: { kategori: string }) {
|
export default function Content({ kategori }: { kategori: string }) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const [animateKey, setAnimateKey] = useState(0);
|
||||||
|
|
||||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||||
const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
|
const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
|
||||||
@@ -37,29 +40,44 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
const paginatedNews = state.findMany.data || [];
|
const paginatedNews = state.findMany.data || [];
|
||||||
const totalPages = state.findMany.totalPages || 1;
|
const totalPages = state.findMany.totalPages || 1;
|
||||||
|
|
||||||
// Load data
|
// Load data awal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
gotongRoyongState.kegiatanDesa.findFirst.load(kategori);
|
gotongRoyongState.kegiatanDesa.findFirst.load(kategori);
|
||||||
}, [kategori]);
|
}, [kategori]);
|
||||||
|
|
||||||
|
// Load daftar berita
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
state.findMany.load(page, 3, '', kategori);
|
state.findMany.load(page, 3, '', kategori);
|
||||||
|
setAnimateKey((prev) => prev + 1); // trigger animasi halus saat page berubah
|
||||||
}, [page, kategori]);
|
}, [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 (
|
return (
|
||||||
<Box py={20}>
|
<Box py={20}>
|
||||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||||
{/* === Gotong Royong Utama === */}
|
{/* === Gotong Royong Utama === */}
|
||||||
{featuredState.loading ? (
|
<Transition mounted={!featuredState.loading} transition="fade" duration={250} timingFunction="ease">
|
||||||
<Center><Skeleton h={400} /></Center>
|
{(styles) => (
|
||||||
) : featured ? (
|
<div style={styles}>
|
||||||
|
{featured ? (
|
||||||
<Box mb={50}>
|
<Box mb={50}>
|
||||||
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
|
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
|
||||||
<Paper shadow="md" radius="md" withBorder>
|
<Paper shadow="md" radius="md" withBorder>
|
||||||
<Grid gutter={0}>
|
<Grid gutter={0}>
|
||||||
<GridCol span={{ base: 12, md: 6 }}>
|
<GridCol span={{ base: 12, md: 6 }}>
|
||||||
<Image
|
<Image
|
||||||
src={featured.image?.link}
|
src={featured.image?.link || '/images/placeholder.jpg'}
|
||||||
alt={featured.judul || 'Berita Utama'}
|
alt={featured.judul || 'Berita Utama'}
|
||||||
height={400}
|
height={400}
|
||||||
fit="cover"
|
fit="cover"
|
||||||
@@ -75,7 +93,12 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
{featured.kategoriKegiatan?.nama || kategori}
|
{featured.kategoriKegiatan?.nama || kategori}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Title order={2} mb="md">{featured.judul}</Title>
|
<Title order={2} mb="md">{featured.judul}</Title>
|
||||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={3}
|
||||||
|
mb="md"
|
||||||
|
dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Group justify="apart" mt="auto">
|
<Group justify="apart" mt="auto">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
@@ -91,7 +114,9 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
rightSection={<IconArrowRight size={16} />}
|
rightSection={<IconArrowRight size={16} />}
|
||||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Baca Selengkapnya
|
Baca Selengkapnya
|
||||||
</Button>
|
</Button>
|
||||||
@@ -101,21 +126,41 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : (
|
||||||
|
<Skeleton h={400} radius="md" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
{/* === Daftar Gotong Royong === */}
|
{/* === Daftar Gotong Royong (Pagination + Fade-in Halus) === */}
|
||||||
<Box mt={50}>
|
<Box mt={50}>
|
||||||
<Title order={2} mb="md">Daftar Gotong Royong</Title>
|
<Title order={2} mb="md">Daftar Gotong Royong</Title>
|
||||||
<Divider mb="xl" />
|
<Divider mb="xl" />
|
||||||
|
|
||||||
|
<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 ? (
|
{state.findMany.loading ? (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||||
{Array(3).fill(0).map((_, i) => (
|
{Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
<Skeleton key={i} h={300} radius="md" />
|
<Skeleton key={i} h={300} radius="md" />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
) : paginatedNews.length === 0 ? (
|
) : paginatedNews.length === 0 ? (
|
||||||
<Text c="dimmed" ta="center">Belum ada gotong royong di kategori "{kategori}".</Text>
|
<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">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||||
{paginatedNews.map((item) => (
|
{paginatedNews.map((item) => (
|
||||||
@@ -129,13 +174,28 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
|
<Image
|
||||||
|
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||||
|
height={200}
|
||||||
|
alt={item.judul}
|
||||||
|
fit="cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Badge color="blue" variant="light" mt="md">
|
<Badge color="blue" variant="light" mt="md">
|
||||||
{item.kategoriKegiatan?.nama || kategori}
|
{item.kategoriKegiatan?.nama || kategori}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
|
||||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
|
{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">
|
<Group justify="apart" mt="md" gap="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
@@ -150,6 +210,8 @@ export default function Content({ kategori }: { kategori: string }) {
|
|||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<Center mt="xl">
|
<Center mt="xl">
|
||||||
|
|||||||
@@ -1,323 +1,5 @@
|
|||||||
// 'use client'
|
|
||||||
// import colors from '@/con/colors';
|
|
||||||
// import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
|
||||||
// import { IconSearch } from '@tabler/icons-react';
|
|
||||||
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
// import React, { useEffect, useState } from 'react';
|
|
||||||
// import BackButton from '../../../desa/layanan/_com/BackButto';
|
|
||||||
|
|
||||||
|
|
||||||
// type HeaderSearchProps = {
|
|
||||||
// placeholder?: string;
|
|
||||||
// searchIcon?: React.ReactNode;
|
|
||||||
// value?: string;
|
|
||||||
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
// children?: React.ReactNode;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// function LayoutTabsGotongRoyong({
|
|
||||||
// children,
|
|
||||||
// placeholder = "pencarian",
|
|
||||||
// searchIcon = <IconSearch size={20} />
|
|
||||||
// }: HeaderSearchProps) {
|
|
||||||
// const router = useRouter();
|
|
||||||
// const pathname = usePathname();
|
|
||||||
// const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
// // Get active tab from URL path
|
|
||||||
// const activeTab = pathname.split('/').pop() || 'semua';
|
|
||||||
|
|
||||||
// // Get initial search value from URL
|
|
||||||
// const initialSearch = searchParams.get('search') || '';
|
|
||||||
// const [searchValue, setSearchValue] = useState(initialSearch);
|
|
||||||
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// // Update active tab state when pathname changes
|
|
||||||
// const [activeTabState, setActiveTabState] = useState(activeTab);
|
|
||||||
// useEffect(() => {
|
|
||||||
// setActiveTabState(activeTab);
|
|
||||||
// }, [activeTab]);
|
|
||||||
|
|
||||||
// // Clean up timeouts on unmount
|
|
||||||
// useEffect(() => {
|
|
||||||
// return () => {
|
|
||||||
// if (searchTimeout !== null) {
|
|
||||||
// clearTimeout(searchTimeout);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }, [searchTimeout]);
|
|
||||||
|
|
||||||
// // Handle search input change with debounce
|
|
||||||
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// const value = event.target.value;
|
|
||||||
// setSearchValue(value);
|
|
||||||
|
|
||||||
// // Clear previous timeout
|
|
||||||
// if (searchTimeout !== null) {
|
|
||||||
// clearTimeout(searchTimeout);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Set new timeout
|
|
||||||
// const newTimeout = window.setTimeout(() => {
|
|
||||||
// const params = new URLSearchParams(searchParams.toString());
|
|
||||||
|
|
||||||
// if (value) {
|
|
||||||
// params.set('search', value);
|
|
||||||
// } else {
|
|
||||||
// params.delete('search');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Only update URL if the search value has actually changed
|
|
||||||
// if (params.toString() !== searchParams.toString()) {
|
|
||||||
// router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
|
|
||||||
// }
|
|
||||||
// }, 500); // 500ms debounce delay
|
|
||||||
|
|
||||||
// setSearchTimeout(newTimeout);
|
|
||||||
// };
|
|
||||||
// const tabs = [
|
|
||||||
// {
|
|
||||||
// label: "Semua",
|
|
||||||
// value: "semua",
|
|
||||||
// href: "/darmasaba/lingkungan/gotong-royong/semua"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Kebersihan",
|
|
||||||
// value: "kebersihan",
|
|
||||||
// href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Infrastruktur",
|
|
||||||
// value: "infrastruktur",
|
|
||||||
// href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Sosial",
|
|
||||||
// value: "sosial",
|
|
||||||
// href: "/darmasaba/lingkungan/gotong-royong/sosial"
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: "Lingkungan",
|
|
||||||
// value: "lingkungan",
|
|
||||||
// href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
|
|
||||||
// }
|
|
||||||
// ];
|
|
||||||
// const handleTabChange = (value: string | null) => {
|
|
||||||
// if (!value) return;
|
|
||||||
// const tab = tabs.find(t => t.value === value);
|
|
||||||
// if (tab) {
|
|
||||||
// const params = new URLSearchParams(searchParams.toString());
|
|
||||||
// router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
|
||||||
// {/* Header */}
|
|
||||||
// <Box px={{ base: "md", md: 100 }}>
|
|
||||||
// <BackButton />
|
|
||||||
// </Box>
|
|
||||||
// <Container size="lg" px="md">
|
|
||||||
// <Stack align="center" gap="0" >
|
|
||||||
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
|
||||||
// Gotong Royong Desa Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// <Text ta="center" px="md">
|
|
||||||
// Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// </Stack>
|
|
||||||
// </Container>
|
|
||||||
|
|
||||||
// <Tabs
|
|
||||||
// color={colors['blue-button']}
|
|
||||||
// variant="pills"
|
|
||||||
// value={activeTabState}
|
|
||||||
// onChange={handleTabChange}
|
|
||||||
// >
|
|
||||||
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
|
||||||
// <Grid>
|
|
||||||
// <GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
|
|
||||||
// <TabsList>
|
|
||||||
// {tabs.map((tab, index) => (
|
|
||||||
// <TabsTab
|
|
||||||
// key={index}
|
|
||||||
// value={tab.value}
|
|
||||||
// onClick={() => router.push(tab.href)}
|
|
||||||
// >
|
|
||||||
// {tab.label}
|
|
||||||
// </TabsTab>
|
|
||||||
// ))}
|
|
||||||
// </TabsList>
|
|
||||||
// </GridCol>
|
|
||||||
// <GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
|
|
||||||
// <TextInput
|
|
||||||
// radius="lg"
|
|
||||||
// placeholder={placeholder}
|
|
||||||
// leftSection={searchIcon}
|
|
||||||
// w="100%"
|
|
||||||
// value={searchValue}
|
|
||||||
// onChange={handleSearchChange}
|
|
||||||
// />
|
|
||||||
// </GridCol>
|
|
||||||
// </Grid>
|
|
||||||
// </Box>
|
|
||||||
|
|
||||||
// {children}
|
|
||||||
// </Tabs>
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default LayoutTabsGotongRoyong;
|
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
// 'use client'
|
|
||||||
// import colors from '@/con/colors';
|
|
||||||
// import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
|
||||||
// import { IconSearch } from '@tabler/icons-react';
|
|
||||||
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
// import React, { useEffect, useState } from 'react';
|
|
||||||
// import BackButton from '../../layanan/_com/BackButto';
|
|
||||||
|
|
||||||
// type HeaderSearchProps = {
|
|
||||||
// placeholder?: string;
|
|
||||||
// searchIcon?: React.ReactNode;
|
|
||||||
// value?: string;
|
|
||||||
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
// children?: React.ReactNode;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// function LayoutTabsBerita({
|
|
||||||
// children,
|
|
||||||
// placeholder = "pencarian",
|
|
||||||
// searchIcon = <IconSearch size={20} />
|
|
||||||
// }: HeaderSearchProps) {
|
|
||||||
// const router = useRouter();
|
|
||||||
// const pathname = usePathname();
|
|
||||||
// const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
// const activeTab = pathname.split('/').pop() || 'semua';
|
|
||||||
// const initialSearch = searchParams.get('search') || '';
|
|
||||||
// const [searchValue, setSearchValue] = useState(initialSearch);
|
|
||||||
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// const [activeTabState, setActiveTabState] = useState(activeTab);
|
|
||||||
// useEffect(() => {
|
|
||||||
// setActiveTabState(activeTab);
|
|
||||||
// }, [activeTab]);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// return () => {
|
|
||||||
// if (searchTimeout !== null) clearTimeout(searchTimeout);
|
|
||||||
// };
|
|
||||||
// }, [searchTimeout]);
|
|
||||||
|
|
||||||
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// const value = event.target.value;
|
|
||||||
// setSearchValue(value);
|
|
||||||
|
|
||||||
// if (searchTimeout !== null) clearTimeout(searchTimeout);
|
|
||||||
|
|
||||||
// const newTimeout = window.setTimeout(() => {
|
|
||||||
// const params = new URLSearchParams(searchParams.toString());
|
|
||||||
|
|
||||||
// if (value) params.set('search', value);
|
|
||||||
// else params.delete('search');
|
|
||||||
|
|
||||||
// if (params.toString() !== searchParams.toString()) {
|
|
||||||
// router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
|
|
||||||
// }
|
|
||||||
// }, 500);
|
|
||||||
|
|
||||||
// setSearchTimeout(newTimeout);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const tabs = [
|
|
||||||
// { label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
|
|
||||||
// { label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
|
|
||||||
// { label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
|
|
||||||
// { label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
|
|
||||||
// { label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
|
|
||||||
// { label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
|
|
||||||
// { label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// const handleTabChange = (value: string | null) => {
|
|
||||||
// if (!value) return;
|
|
||||||
// const tab = tabs.find(t => t.value === value);
|
|
||||||
// if (tab) {
|
|
||||||
// const params = new URLSearchParams(searchParams.toString());
|
|
||||||
// router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
|
||||||
// {/* Header */}
|
|
||||||
// <Box px={{ base: "md", md: 100 }}>
|
|
||||||
// <BackButton />
|
|
||||||
// </Box>
|
|
||||||
|
|
||||||
// <Box px={{ base: 'md', md: 100 }}>
|
|
||||||
// <Group justify='space-between' align="center">
|
|
||||||
// <Stack gap="0">
|
|
||||||
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
|
|
||||||
// Portal Berita Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// <Text>
|
|
||||||
// Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
|
|
||||||
// </Text>
|
|
||||||
// </Stack>
|
|
||||||
// <Box>
|
|
||||||
// <TextInput
|
|
||||||
// radius="lg"
|
|
||||||
// placeholder={placeholder}
|
|
||||||
// leftSection={searchIcon}
|
|
||||||
// w="100%"
|
|
||||||
// value={searchValue}
|
|
||||||
// onChange={handleSearchChange}
|
|
||||||
// />
|
|
||||||
// </Box>
|
|
||||||
// </Group>
|
|
||||||
// </Box>
|
|
||||||
|
|
||||||
// <Tabs
|
|
||||||
// color={colors['blue-button']}
|
|
||||||
// variant="pills"
|
|
||||||
// value={activeTabState}
|
|
||||||
// onChange={handleTabChange}
|
|
||||||
// >
|
|
||||||
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
|
||||||
// {/* SCROLLABLE TABS */}
|
|
||||||
// <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
|
|
||||||
// <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
|
|
||||||
// {tabs.map((tab, index) => (
|
|
||||||
// <TabsTab
|
|
||||||
// key={index}
|
|
||||||
// value={tab.value}
|
|
||||||
// onClick={() => router.push(tab.href)}
|
|
||||||
// style={{
|
|
||||||
// flex: '0 0 auto', // Prevent shrinking
|
|
||||||
// minWidth: 100, // optional: makes them touch-friendly
|
|
||||||
// textAlign: 'center'
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// {tab.label}
|
|
||||||
// </TabsTab>
|
|
||||||
// ))}
|
|
||||||
// </TabsList>
|
|
||||||
// </Box>
|
|
||||||
// </Box>
|
|
||||||
|
|
||||||
// {children}
|
|
||||||
// </Tabs>
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default LayoutTabsBerita;
|
|
||||||
|
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||||
@@ -402,7 +84,7 @@ function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) {
|
|||||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||||
Portal Gotong royong Darmasaba
|
Portal Gotong royong Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
<Text>Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
|
<Text fz="md">Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -1,62 +1,134 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
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 { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||||
import { useTransitionRouter } from 'next-view-transitions';
|
import { motion } from 'framer-motion';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function Page() {
|
export default function Page() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useTransitionRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Parameter URL
|
|
||||||
const search = searchParams.get('search') || '';
|
const search = searchParams.get('search') || '';
|
||||||
const page = parseInt(searchParams.get('page') || '1');
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
|
||||||
// Gunakan proxy untuk state
|
|
||||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
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 loadingGrid = state.findMany.loading;
|
||||||
const loadingFeatured = featured.loading;
|
const loadingFeatured = featured.loading;
|
||||||
|
|
||||||
// Load berita utama (hanya sekali)
|
// Load featured data once on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!featured.data && !loadingFeatured) {
|
let mounted = true;
|
||||||
gotongRoyongState.kegiatanDesa.findFirst.load();
|
|
||||||
}
|
const loadFeatured = async () => {
|
||||||
}, [featured.data, loadingFeatured]);
|
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(() => {
|
useEffect(() => {
|
||||||
const limit = 3; // Sesuaikan dengan tampilan grid
|
let mounted = true;
|
||||||
state.findMany.load(page, limit, search);
|
|
||||||
|
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]);
|
}, [page, search]);
|
||||||
|
|
||||||
// Update URL saat page berubah
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
const url = new URLSearchParams(searchParams.toString());
|
const url = new URLSearchParams(searchParams.toString());
|
||||||
if (search) url.set('search', search);
|
if (search) url.set('search', search);
|
||||||
if (newPage > 1) url.set('page', newPage.toString());
|
if (newPage > 1) url.set('page', newPage.toString());
|
||||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
else url.delete('page');
|
||||||
|
|
||||||
router.replace(`?${url.toString()}`);
|
// Use push instead of replace to keep browser history
|
||||||
|
router.push(`?${url.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const featuredData = featured.data;
|
const featuredData = featured.data;
|
||||||
const paginatedNews = state.findMany.data || [];
|
const paginatedNews = state.findMany.data || [];
|
||||||
const totalPages = state.findMany.totalPages || 1;
|
const totalPages = state.findMany.totalPages || 1;
|
||||||
|
|
||||||
|
// Animasi transisi halus tapi tetap instant load
|
||||||
|
const MotionBox = motion(Box as any);
|
||||||
|
|
||||||
|
// fallback kosong
|
||||||
|
if (!loadingGrid && !loadingFeatured && paginatedNews.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box py={20}>
|
<Container size="xl" py={80} ta="center">
|
||||||
<Container size="xl" px={{ base: "md", md: "xl" }}>
|
<Title order={2} mb="md">Belum Ada Data Gotong Royong</Title>
|
||||||
{/* === Gotong royong Utama (Tetap) === */}
|
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
|
||||||
{loadingFeatured ? (
|
</Container>
|
||||||
<Center><Skeleton h={400} /></Center>
|
);
|
||||||
) : featuredData ? (
|
}
|
||||||
<Box mb={50}>
|
|
||||||
|
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>
|
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
|
||||||
<Paper shadow="md" radius="md" withBorder>
|
<Paper shadow="md" radius="md" withBorder>
|
||||||
<Grid gutter={0}>
|
<Grid gutter={0}>
|
||||||
@@ -78,7 +150,12 @@ function Page() {
|
|||||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
|
<Text
|
||||||
|
c="dimmed"
|
||||||
|
lineClamp={3}
|
||||||
|
mb="md"
|
||||||
|
dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Group justify="apart" mt="auto">
|
<Group justify="apart" mt="auto">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
@@ -87,14 +164,18 @@ function Page() {
|
|||||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
rightSection={<IconArrowRight size={16} />}
|
rightSection={<IconArrowRight size={16} />}
|
||||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`)}
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Baca Selengkapnya
|
Baca Selengkapnya
|
||||||
</Button>
|
</Button>
|
||||||
@@ -104,31 +185,36 @@ function Page() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : (
|
||||||
|
<Skeleton h={400} radius="md" mb="xl" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
{/* === Gotong royong Terbaru (Berubah Saat Pagination) === */}
|
{/* === Gotong royong Terbaru === */}
|
||||||
<Box mt={50}>
|
<Box mt={50}>
|
||||||
<Title order={2} mb="md">Gotong royong Terbaru</Title>
|
<Title order={2} mb="md">Gotong royong Terbaru</Title>
|
||||||
<Divider mb="xl" />
|
<Divider mb="xl" />
|
||||||
|
|
||||||
{loadingGrid ? (
|
<Transition mounted={!loadingGrid} transition="fade" duration={200} timingFunction="ease-out">
|
||||||
|
{(styles) =>
|
||||||
|
loadingGrid ? (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||||
{Array(3).fill(0).map((_, i) => (
|
{Array(3)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
<Skeleton key={i} h={300} radius="md" />
|
<Skeleton key={i} h={300} radius="md" />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
) : paginatedNews.length === 0 ? (
|
) : paginatedNews.length === 0 ? (
|
||||||
<Text c="dimmed" ta="center">Tidak ada gotong royong ditemukan.</Text>
|
<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">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||||
{paginatedNews.map((item) => (
|
{paginatedNews.map((item) => (
|
||||||
<Card
|
<Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
|
||||||
key={item.id}
|
|
||||||
shadow="sm"
|
|
||||||
p="lg"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
>
|
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image
|
<Image
|
||||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||||
@@ -143,27 +229,49 @@ function Page() {
|
|||||||
{item.kategoriKegiatan?.nama || 'Gotong royong'}
|
{item.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||||
</Badge>
|
</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">
|
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
year: 'numeric'
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
</Text>
|
</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>
|
<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>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
{/* Pagination hanya untuk berita terbaru */}
|
{/* Pagination */}
|
||||||
<Center mt="xl">
|
<Center mt="xl">
|
||||||
<Pagination
|
<Pagination
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
@@ -176,9 +284,6 @@ function Page() {
|
|||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</MotionBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||||
import colors from '@/con/colors';
|
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 { 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 React, { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
@@ -122,8 +122,16 @@ function Page() {
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{data2?.map((v, k) => (
|
{data2?.map((v, k) => (
|
||||||
<Paper key={k} p="md" withBorder radius="md">
|
<Paper key={k} p="md" withBorder radius="md">
|
||||||
|
<Group justify='space-between'>
|
||||||
|
<Box>
|
||||||
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
|
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
|
||||||
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</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 ? (
|
{v.lat && v.lng ? (
|
||||||
<a
|
<a
|
||||||
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
|
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
|
||||||
@@ -131,7 +139,7 @@ function Page() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
style={{ color: colors['blue-button'], textDecoration: 'none' }}
|
style={{ color: colors['blue-button'], textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
<Text fz="sm">📌 Buka di Google Maps</Text>
|
<Text fz="sm">📌 Lihat Peta Lebih Besar</Text>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
|
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
|
||||||
|
|||||||
@@ -66,8 +66,11 @@ function Page() {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
<Text c="dimmed" fz={{ base: 'sm', md: 'lg' }} mt="sm">
|
<Text fz="md" mt="sm">
|
||||||
Mari berpartisipasi menanam dan merawat pohon untuk menciptakan lingkungan hijau, sehat, dan seimbang bagi seluruh warga desa.
|
Mari berpartisipasi menanam dan merawat pohon untuk menciptakan lingkungan hijau,
|
||||||
|
</Text>
|
||||||
|
<Text fz="md">
|
||||||
|
sehat, dan seimbang bagi seluruh warga desa.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={60}>
|
<Box px={{ base: 'md', md: 100 }} pb={60}>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function Page() {
|
|||||||
<Title fz={55} fw={900} c={colors['blue-button']}>
|
<Title fz={55} fw={900} c={colors['blue-button']}>
|
||||||
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
|
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
|
||||||
</Title>
|
</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.
|
Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
|
||||||
</Text>
|
</Text>
|
||||||
<Group mt="xl">
|
<Group mt="xl">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Timeline,
|
Timeline,
|
||||||
Title
|
Title
|
||||||
} from '@mantine/core';
|
} 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 { useRouter } from 'next/navigation';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -69,10 +69,11 @@ export default function BeasiswaPage() {
|
|||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
<Stack gap="md" maw={600}>
|
<Stack gap="md" maw={600}>
|
||||||
<Title order={2} c="blue.9">
|
<Group>
|
||||||
Program Beasiswa Pendidikan Desa Darmasaba
|
<IconSchool size={30} color={colors["blue-button"]} />
|
||||||
</Title>
|
<Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
|
||||||
<Text c="dimmed">
|
</Group>
|
||||||
|
<Text>
|
||||||
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
|
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
|
||||||
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
|
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -81,21 +82,35 @@ export default function BeasiswaPage() {
|
|||||||
|
|
||||||
{/* Tentang Program */}
|
{/* Tentang Program */}
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
<Title order={3} mb="sm">
|
<Group mb="sm">
|
||||||
Tentang Program
|
<IconInfoCircle size={24} color={colors["blue-button"]} />
|
||||||
</Title>
|
<Title order={3}>Tentang Program</Title>
|
||||||
|
</Group>
|
||||||
<Text>
|
<Text>
|
||||||
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
|
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
|
||||||
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
|
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
|
||||||
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
|
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
|
||||||
</Text>
|
</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>
|
</Container>
|
||||||
|
|
||||||
{/* Syarat dan Ketentuan */}
|
{/* Syarat dan Ketentuan */}
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
<Title order={3} mb="sm">
|
<Group mb="sm">
|
||||||
Syarat Pendaftaran
|
<IconChecklist size={24} color={colors["blue-button"]} />
|
||||||
</Title>
|
<Title order={3}>Syarat Pendaftaran</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||||
<Paper shadow="sm" p="md" radius="lg" withBorder>
|
<Paper shadow="sm" p="md" radius="lg" withBorder>
|
||||||
@@ -123,42 +138,61 @@ export default function BeasiswaPage() {
|
|||||||
|
|
||||||
{/* Proses Seleksi */}
|
{/* Proses Seleksi */}
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
<Title order={3} mb="sm">
|
<Group mb="sm">
|
||||||
Proses Seleksi
|
<IconTimeline size={24} color={colors["blue-button"]} />
|
||||||
</Title>
|
<Title order={3}>Proses Seleksi</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Timeline active={4} bulletSize={24} lineWidth={2}>
|
<Timeline active={4} bulletSize={24} lineWidth={2}>
|
||||||
<Timeline.Item title="Pendaftaran Online">
|
<Timeline.Item title="Pendaftaran Online">
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
|
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||||
|
⏰ Estimasi waktu: 1 Februari – 31 Mei 2025
|
||||||
|
</Text>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
|
|
||||||
<Timeline.Item title="Seleksi Administrasi">
|
<Timeline.Item title="Seleksi Administrasi">
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
Panitia memverifikasi kelengkapan dan validitas berkas.
|
Panitia memverifikasi kelengkapan dan validitas berkas.
|
||||||
</Text>
|
</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>
|
||||||
|
|
||||||
<Timeline.Item title="Wawancara dan Penilaian">
|
<Timeline.Item title="Wawancara dan Penilaian">
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
|
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
|
||||||
</Text>
|
</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>
|
||||||
|
|
||||||
<Timeline.Item title="Pengumuman Penerima">
|
<Timeline.Item title="Pengumuman Penerima">
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
|
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
|
||||||
</Text>
|
</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.Item>
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
|
||||||
|
<Text c="dimmed" size="sm" mt="lg" ta="center">
|
||||||
|
🗓️ Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran
|
||||||
|
</Text>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
|
||||||
{/* Testimoni */}
|
{/* Testimoni */}
|
||||||
<Container size="lg" py="xl">
|
<Container size="lg" py="xl">
|
||||||
<Title order={3} mb="sm">
|
<Group mb="sm">
|
||||||
Cerita Sukses Penerima Beasiswa
|
<IconQuote size={24} color={colors["blue-button"]} />
|
||||||
</Title>
|
<Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||||
<Paper shadow="md" p="lg" radius="lg">
|
<Paper shadow="md" p="lg" radius="lg">
|
||||||
@@ -183,7 +217,10 @@ export default function BeasiswaPage() {
|
|||||||
|
|
||||||
{/* CTA Akhir */}
|
{/* CTA Akhir */}
|
||||||
<Container size="lg" py="xl" ta="center">
|
<Container size="lg" py="xl" ta="center">
|
||||||
|
<Group justify="center" mb="sm">
|
||||||
|
<IconUserPlus size={28} color={colors["blue-button"]} />
|
||||||
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
|
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
|
||||||
|
</Group>
|
||||||
<Text c="dimmed" mb="md">
|
<Text c="dimmed" mb="md">
|
||||||
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
|
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||||
import colors from '@/con/colors';
|
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 { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
|
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
|
||||||
@@ -49,46 +49,46 @@ function Page() {
|
|||||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
|
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
|
||||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Box>
|
<Group>
|
||||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
|
||||||
{stateTujuanProgram.findById.data?.judul}
|
|
||||||
</Badge>
|
|
||||||
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
|
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
|
||||||
<Box>
|
<Box>
|
||||||
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
|
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</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 }} />
|
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Box>
|
<Group>
|
||||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
|
||||||
{stateLokasiDanJadwal.findById.data?.judul}
|
|
||||||
</Badge>
|
|
||||||
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
|
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
|
||||||
<Box>
|
<Box>
|
||||||
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
|
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</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 }} />
|
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Box>
|
<Group>
|
||||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
|
||||||
{stateFasilitas.findById.data?.judul}
|
|
||||||
</Badge>
|
|
||||||
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
|
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
|
||||||
<Box>
|
<Box>
|
||||||
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
|
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</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 }} />
|
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ function Page() {
|
|||||||
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
|
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<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>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function Page({ params }: PageProps) {
|
|||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh w="30%">Nama Pengajar</TableTh>
|
<TableTh w="30%">Nama Pengajar</TableTh>
|
||||||
<TableTh w="30%">Nama Lembaga</TableTh>
|
<TableTh w="30%">Nama Lembaga</TableTh>
|
||||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
<TableTh w="40%">Mengajar Di Jenjang Pendidikan</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
|
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
|
||||||
import colors from '@/con/colors';
|
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 { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
|
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
|
||||||
@@ -43,7 +43,7 @@ function Page() {
|
|||||||
Pendidikan Non Formal
|
Pendidikan Non Formal
|
||||||
</Title>
|
</Title>
|
||||||
<Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
|
<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>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
@@ -59,12 +59,16 @@ function Page() {
|
|||||||
withBorder
|
withBorder
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<Group align="center" gap={8} wrap="nowrap">
|
||||||
<Tooltip label="Fokus utama program" withArrow>
|
<Tooltip label="Fokus utama program" withArrow>
|
||||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
<Box display="flex" style={{ alignItems: "center" }}>
|
||||||
<IconTarget size={28} style={{ marginRight: 8 }} />
|
<IconTarget color={colors['blue-button']} size={26} />
|
||||||
{stateTujuanPendidikanNonFormal.findById.data?.judul}
|
</Box>
|
||||||
</Title>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||||
|
{stateTujuanPendidikanNonFormal.findById.data?.judul}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
|
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -76,12 +80,16 @@ function Page() {
|
|||||||
withBorder
|
withBorder
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<Group align="center" gap={8} wrap="nowrap">
|
||||||
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
|
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
|
||||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
<Box display="flex" style={{ alignItems: "center" }}>
|
||||||
<IconMapPin size={28} style={{ marginRight: 8 }} />
|
<IconMapPin color={colors['blue-button']} size={26} />
|
||||||
{stateTempatKegiatan.findById.data?.judul}
|
</Box>
|
||||||
</Title>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||||
|
{stateTempatKegiatan.findById.data?.judul}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
|
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -95,12 +103,16 @@ function Page() {
|
|||||||
withBorder
|
withBorder
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<Group align="center" gap={8} wrap="nowrap">
|
||||||
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
|
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
|
||||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
<Box display="flex" style={{ alignItems: "center" }}>
|
||||||
<IconBook2 size={28} style={{ marginRight: 8 }} />
|
<IconBook2 color={colors['blue-button']} size={26} />
|
||||||
{stateJenisProgram.findById.data?.judul}
|
</Box>
|
||||||
</Title>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||||
|
{stateJenisProgram.findById.data?.judul}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
|
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -16,16 +16,11 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { DateInput } from '@mantine/dates';
|
import { DateInput } from '@mantine/dates';
|
||||||
import {
|
import { IconArrowRight, IconBook2, IconUser } from '@tabler/icons-react';
|
||||||
IconArrowRight,
|
|
||||||
IconBook2,
|
|
||||||
IconUser
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
|
||||||
export interface ModalPeminjamanProps {
|
export interface ModalPeminjamanProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -45,11 +40,12 @@ export default function ModalPeminjaman({
|
|||||||
}: ModalPeminjamanProps) {
|
}: ModalPeminjamanProps) {
|
||||||
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
|
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
|
||||||
|
|
||||||
// reset form setiap modal dibuka
|
const BATAS_HARI_PINJAM = 4;
|
||||||
|
|
||||||
|
// Reset form setiap modal dibuka
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened && buku) {
|
if (opened && buku) {
|
||||||
perpustakaanDigitalState.peminjamanBuku.create.form = {
|
perpustakaanDigitalState.peminjamanBuku.create.form = {
|
||||||
...perpustakaanDigitalState.peminjamanBuku.create.form,
|
|
||||||
bukuId: buku.id,
|
bukuId: buku.id,
|
||||||
nama: '',
|
nama: '',
|
||||||
noTelp: '',
|
noTelp: '',
|
||||||
@@ -99,7 +95,14 @@ export default function ModalPeminjaman({
|
|||||||
</Badge>
|
</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>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -112,7 +115,8 @@ export default function ModalPeminjaman({
|
|||||||
leftSection={<IconUser size={16} />}
|
leftSection={<IconUser size={16} />}
|
||||||
value={snap.create.form.nama}
|
value={snap.create.form.nama}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value)
|
(perpustakaanDigitalState.peminjamanBuku.create.form.nama =
|
||||||
|
e.currentTarget.value)
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -123,7 +127,8 @@ export default function ModalPeminjaman({
|
|||||||
leftSection={<IconUser size={16} />}
|
leftSection={<IconUser size={16} />}
|
||||||
value={snap.create.form.noTelp}
|
value={snap.create.form.noTelp}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value)
|
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp =
|
||||||
|
e.currentTarget.value)
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -134,11 +139,13 @@ export default function ModalPeminjaman({
|
|||||||
leftSection={<IconUser size={16} />}
|
leftSection={<IconUser size={16} />}
|
||||||
value={snap.create.form.alamat}
|
value={snap.create.form.alamat}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value)
|
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat =
|
||||||
|
e.currentTarget.value)
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */}
|
||||||
<DateInput
|
<DateInput
|
||||||
label="Tanggal Pinjam"
|
label="Tanggal Pinjam"
|
||||||
placeholder="Pilih tanggal pinjam"
|
placeholder="Pilih tanggal pinjam"
|
||||||
@@ -148,64 +155,83 @@ export default function ModalPeminjaman({
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
|
if (date) {
|
||||||
|
const tanggalPinjam = new Date(date);
|
||||||
|
|
||||||
|
// simpan tanggal pinjam
|
||||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
|
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
|
||||||
date ? new Date(date).toISOString() : '';
|
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
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text>Catatan</Text>
|
<Text fw={500}>Catatan</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={snap.create.form.catatan}
|
value={snap.create.form.catatan}
|
||||||
onChange={(e) =>
|
onChange={(val) =>
|
||||||
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e)
|
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan =
|
||||||
|
val)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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
|
<DateInput
|
||||||
label="Batas Pengembalian"
|
label="Batas Pengembalian"
|
||||||
placeholder="Pilih tanggal kembali"
|
placeholder="Otomatis diatur +4 hari dari tanggal pinjam"
|
||||||
value={
|
value={
|
||||||
snap.create.form.batasKembali
|
snap.create.form.batasKembali
|
||||||
? new Date(snap.create.form.batasKembali)
|
? new Date(snap.create.form.batasKembali)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(date) => {
|
disabled
|
||||||
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
|
readOnly
|
||||||
date ? new Date(date).toISOString() : '';
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={snap.create.loading}
|
loading={snap.create.loading}
|
||||||
disabled={
|
disabled={
|
||||||
!snap.create.form.nama ||
|
!snap.create.form.nama || !snap.create.form.tanggalPinjam
|
||||||
!snap.create.form.tanggalPinjam ||
|
|
||||||
!snap.create.form.batasKembali ||
|
|
||||||
!snap.create.form.tanggalKembali
|
|
||||||
}
|
}
|
||||||
rightSection={<IconArrowRight size={16} />}
|
rightSection={<IconArrowRight size={16} />}
|
||||||
radius="xl"
|
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
|
Pinjam Buku
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Content() {
|
|||||||
try {
|
try {
|
||||||
await state.dataPerpustakaan.findMany.load(
|
await state.dataPerpustakaan.findMany.load(
|
||||||
currentPage,
|
currentPage,
|
||||||
10,
|
3,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Dasar Hukum
|
Dasar Hukum
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta="center" fz="sm" c="dimmed">
|
<Text ta="center" fz="md" >
|
||||||
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
|
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
|
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import { BarChart, PieChart } from '@mantine/charts';
|
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 { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||||
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
@@ -23,9 +23,8 @@ function Kepuasan() {
|
|||||||
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
||||||
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
||||||
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = 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 [opened, { open, close }] = useDisclosure(false)
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
state.create.form = {
|
state.create.form = {
|
||||||
@@ -122,18 +121,18 @@ function Kepuasan() {
|
|||||||
|
|
||||||
// Convert map to array and sort by date
|
// Convert map to array and sort by date
|
||||||
const barData = Array.from(monthYearMap.entries())
|
const barData = Array.from(monthYearMap.entries())
|
||||||
.map(([key, count]) => {
|
.map(([key, Responden]) => {
|
||||||
const [year, month] = key.split('-');
|
const [year, month] = key.split('-');
|
||||||
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
||||||
.toLocaleString('id-ID', { month: 'long' });
|
.toLocaleString('id-ID', { month: 'long' });
|
||||||
return {
|
return {
|
||||||
month: `${monthName} ${year}`,
|
month: `${monthName} ${year}`,
|
||||||
count,
|
Responden,
|
||||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.sortKey - b.sortKey)
|
.sort((a, b) => a.sortKey - b.sortKey)
|
||||||
.map(({ month, count }) => ({ month, count }));
|
.map(({ month, Responden }) => ({ month, Responden }));
|
||||||
|
|
||||||
setBarChartData(barData);
|
setBarChartData(barData);
|
||||||
}
|
}
|
||||||
@@ -141,12 +140,12 @@ function Kepuasan() {
|
|||||||
|
|
||||||
if ((loading && !data) || !data) {
|
if ((loading && !data) || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10} px="sm">
|
<Stack py={10} px="xl">
|
||||||
<Skeleton height={200} mb="md" />
|
<Skeleton height={300} mb="md" />
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||||
<Skeleton height={200} />
|
<Skeleton height={300} />
|
||||||
<Skeleton height={200} />
|
<Skeleton height={300} />
|
||||||
<Skeleton height={200} />
|
<Skeleton height={300} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -157,10 +156,16 @@ function Kepuasan() {
|
|||||||
<Stack p="sm">
|
<Stack p="sm">
|
||||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
||||||
<Center>
|
<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>
|
</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}>
|
<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>
|
</Center>
|
||||||
</Container>
|
</Container>
|
||||||
<Box px={"xl"}>
|
<Box px={"xl"}>
|
||||||
@@ -176,11 +181,274 @@ function Kepuasan() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<BarChart
|
||||||
|
h={window.innerWidth < 480 ? 200 : 300}
|
||||||
|
data={barChartData}
|
||||||
|
dataKey="month"
|
||||||
|
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, lg: 3 }}
|
||||||
|
spacing="md"
|
||||||
|
verticalSpacing="md"
|
||||||
|
>
|
||||||
|
{/* Chart Jenis Kelamin */}
|
||||||
|
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||||
|
<Stack>
|
||||||
|
<Title order={4}>Jenis Kelamin</Title>
|
||||||
|
{donutDataJenisKelamin.every(item => item.value === 0) ? (
|
||||||
|
<Text c="dimmed" ta="center" my="md">
|
||||||
|
Belum ada data untuk ditampilkan dalam grafik
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<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={250} // Fixed size in pixels
|
||||||
|
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>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Chart Rating */}
|
||||||
|
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||||
|
<Stack>
|
||||||
|
<Title order={4}>Pilihan</Title>
|
||||||
|
{donutDataRating.every(item => item.value === 0) ? (
|
||||||
|
<Text c="dimmed" ta="center" my="md">
|
||||||
|
Belum ada data untuk ditampilkan dalam grafik
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<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={250}
|
||||||
|
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>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Chart Kelompok Umur */}
|
||||||
|
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||||
|
<Stack>
|
||||||
|
<Title order={4}>Umur</Title>
|
||||||
|
{donutDataKelompokUmur.every(item => item.value === 0) ? (
|
||||||
|
<Text c="dimmed" ta="center" my="md">
|
||||||
|
Belum ada data untuk ditampilkan dalam grafik
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<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={250}
|
||||||
|
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>
|
||||||
|
</Paper>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
{/* Modal */}
|
||||||
|
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
label="Nama"
|
||||||
|
type='text'
|
||||||
|
placeholder="Masukkan nama"
|
||||||
|
defaultValue={state.create.form.name}
|
||||||
|
onChange={(val) => {
|
||||||
|
state.create.form.name = val.currentTarget.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Tanggal"
|
||||||
|
type="date"
|
||||||
|
placeholder="masukkan tanggal"
|
||||||
|
defaultValue={state.create.form.tanggal}
|
||||||
|
onChange={(val) => {
|
||||||
|
state.create.form.tanggal = val.currentTarget.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
key={"jenisKelamin"}
|
||||||
|
label={"Jenis Kelamin"}
|
||||||
|
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
|
||||||
|
defaultValue={state.create.form.jenisKelaminId || ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
state.create.form.jenisKelaminId = val ?? "";
|
||||||
|
}}
|
||||||
|
data={
|
||||||
|
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
|
||||||
|
.filter(Boolean) // Hapus null, undefined, dll
|
||||||
|
.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name || 'Tanpa Nama',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
key={"rating_responden"}
|
||||||
|
label={"Rating"}
|
||||||
|
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
||||||
|
defaultValue={state.create.form.ratingId || ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
state.create.form.ratingId = val ?? "";
|
||||||
|
}}
|
||||||
|
data={
|
||||||
|
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||||
|
.filter(Boolean) // Hapus null, undefined, dll
|
||||||
|
.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name || 'Tanpa Nama',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
key={"kelompokUmur"}
|
||||||
|
label={"Kelompok Umur"}
|
||||||
|
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
|
||||||
|
defaultValue={state.create.form.kelompokUmurId || ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
state.create.form.kelompokUmurId = val ?? "";
|
||||||
|
}}
|
||||||
|
data={
|
||||||
|
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
|
||||||
|
.filter(Boolean) // Hapus null, undefined, dll
|
||||||
|
.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name || 'Tanpa Nama',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
mt={10}
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Modal>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<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={"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
|
<BarChart
|
||||||
h={300}
|
h={300}
|
||||||
data={barChartData}
|
data={barChartData}
|
||||||
dataKey="month"
|
dataKey="month"
|
||||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||||
tickLine="y"
|
tickLine="y"
|
||||||
xAxisLabel="Bulan"
|
xAxisLabel="Bulan"
|
||||||
yAxisLabel="Jumlah Responden"
|
yAxisLabel="Jumlah Responden"
|
||||||
@@ -337,212 +605,7 @@ function Kepuasan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Tanggal"
|
label="Tanggal Pengisian"
|
||||||
type="date"
|
|
||||||
placeholder="masukkan tanggal"
|
|
||||||
defaultValue={state.create.form.tanggal}
|
|
||||||
onChange={(val) => {
|
|
||||||
state.create.form.tanggal = val.currentTarget.value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
key={"jenisKelamin"}
|
|
||||||
label={"Jenis Kelamin"}
|
|
||||||
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
|
|
||||||
defaultValue={state.create.form.jenisKelaminId || ""}
|
|
||||||
onChange={(val) => {
|
|
||||||
state.create.form.jenisKelaminId = val ?? "";
|
|
||||||
}}
|
|
||||||
data={
|
|
||||||
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
|
|
||||||
.filter(Boolean) // Hapus null, undefined, dll
|
|
||||||
.map((item) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: item.name || 'Tanpa Nama',
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
key={"rating_responden"}
|
|
||||||
label={"Rating"}
|
|
||||||
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
|
||||||
defaultValue={state.create.form.ratingId || ""}
|
|
||||||
onChange={(val) => {
|
|
||||||
state.create.form.ratingId = val ?? "";
|
|
||||||
}}
|
|
||||||
data={
|
|
||||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
|
||||||
.filter(Boolean) // Hapus null, undefined, dll
|
|
||||||
.map((item) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: item.name || 'Tanpa Nama',
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
key={"kelompokUmur"}
|
|
||||||
label={"Kelompok Umur"}
|
|
||||||
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
|
|
||||||
defaultValue={state.create.form.kelompokUmurId || ""}
|
|
||||||
onChange={(val) => {
|
|
||||||
state.create.form.kelompokUmurId = val ?? "";
|
|
||||||
}}
|
|
||||||
data={
|
|
||||||
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
|
|
||||||
.filter(Boolean) // Hapus null, undefined, dll
|
|
||||||
.map((item) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: item.name || 'Tanpa Nama',
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
mt={10}
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Modal>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
</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")}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
<BarChart
|
|
||||||
h={isMobile ? 200 : 300}
|
|
||||||
data={barChartData}
|
|
||||||
dataKey="month"
|
|
||||||
series={[{ name: "count", color: colors["blue-button"] }]}
|
|
||||||
withTooltip
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
<Box py="xl">
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg">
|
|
||||||
{/* Chart Jenis Kelamin */}
|
|
||||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
|
||||||
<Stack>
|
|
||||||
<Title order={4}>Jenis Kelamin</Title>
|
|
||||||
{donutDataJenisKelamin.every(item => item.value === 0) ? (
|
|
||||||
<Text c="dimmed" ta="center" my="md">
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Chart Rating */}
|
|
||||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
|
||||||
<Stack>
|
|
||||||
<Title order={4}>Pilihan</Title>
|
|
||||||
{donutDataRating.every(item => item.value === 0) ? (
|
|
||||||
<Text c="dimmed" ta="center" my="md">
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Chart Kelompok Umur */}
|
|
||||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
|
||||||
<Stack>
|
|
||||||
<Title order={4}>Umur</Title>
|
|
||||||
{donutDataKelompokUmur.every(item => item.value === 0) ? (
|
|
||||||
<Text c="dimmed" ta="center" my="md">
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
{/* Modal */}
|
|
||||||
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
|
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<TextInput
|
|
||||||
label="Nama"
|
|
||||||
type='text'
|
|
||||||
placeholder="masukkan nama"
|
|
||||||
defaultValue={state.create.form.name}
|
|
||||||
onChange={(val) => {
|
|
||||||
state.create.form.name = val.currentTarget.value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Tanggal"
|
|
||||||
type="date"
|
type="date"
|
||||||
placeholder="masukkan tanggal"
|
placeholder="masukkan tanggal"
|
||||||
defaultValue={state.create.form.tanggal}
|
defaultValue={state.create.form.tanggal}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function JenisInformasiSelector({ onChange }: {
|
|||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
<Select
|
<Select
|
||||||
placeholder='pilih jenis informasi'
|
placeholder='Pilih jenis informasi'
|
||||||
label='Jenis Informasi'
|
label='Jenis Informasi'
|
||||||
data={data.map((item) => ({
|
data={data.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function MemperolehInformasi({ onChange }: {
|
|||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
<Select
|
<Select
|
||||||
placeholder='pilih cara memperoleh informasi'
|
placeholder='Pilih cara memperoleh informasi'
|
||||||
label={"Cara Memperoleh Informasi"}
|
label={"Cara Memperoleh Informasi"}
|
||||||
data={data.map((item) => ({
|
data={data.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function MemperolehSalinan({ onChange }: {
|
|||||||
return (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
<Select
|
<Select
|
||||||
placeholder='pilih cara memperoleh salinan informasi'
|
placeholder='Pilih cara memperoleh salinan informasi'
|
||||||
label={'Cara Memperoleh Salinan Informasi'}
|
label={'Cara Memperoleh Salinan Informasi'}
|
||||||
data={data.map((item) => ({
|
data={data.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ function Page() {
|
|||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Alamat Email"
|
label="Alamat Email"
|
||||||
placeholder="contoh: nama@email.com"
|
placeholder="Contoh: nama@email.com"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -190,7 +190,7 @@ function Page() {
|
|||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nomor Telepon"
|
label="Nomor Telepon"
|
||||||
placeholder="contoh: 0812-3456-7890"
|
placeholder="Contoh: 0812-3456-7890"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
|
|||||||
@@ -51,31 +51,37 @@ function Page() {
|
|||||||
<Box key={item.id} px={{ base: "md", md: 100 }}>
|
<Box key={item.id} px={{ base: "md", md: 100 }}>
|
||||||
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
|
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Flex align="center" gap={40} justify="center">
|
<Center>
|
||||||
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
|
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} w={{ base: 70, md: 120 }} alt="Logo Desa" />
|
||||||
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
|
</Center>
|
||||||
Pejabat Pengelola Informasi Publik
|
<Text ta="center" fz={{ base: "1.2rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
|
||||||
|
Pejabat Pengelola Informasi dan Dokumentasi
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
<Box px={{ base: 0, md: 50 }} pb={40}>
|
<Box px={{ base: 0, md: 50 }} pb={40}>
|
||||||
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
|
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
|
||||||
<Box px={{ base: 0, md: 50 }}>
|
<Box px={{ base: 0, md: 50 }}>
|
||||||
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
|
<Paper bg={colors['white-trans-1']} radius="xl" shadow="md" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap={0}>
|
||||||
<Center>
|
|
||||||
<Image
|
<Image
|
||||||
loading='lazy'
|
pt={{ base: 0, md: 100 }}
|
||||||
|
px="lg"
|
||||||
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
|
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
|
||||||
w={{ base: 220, md: 330 }}
|
|
||||||
alt="Foto Pimpinan"
|
alt="Foto Pimpinan"
|
||||||
radius="md"
|
radius="lg"
|
||||||
|
onError={(e) => e.currentTarget.src = "/perbekel.png"}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</Center>
|
<Paper
|
||||||
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
|
bg={colors['blue-button']}
|
||||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
|
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}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
||||||
|
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -15,17 +17,25 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Transition
|
||||||
Transition,
|
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
|
import {
|
||||||
|
IconArrowsMaximize,
|
||||||
|
IconArrowsMinimize,
|
||||||
|
IconRefresh,
|
||||||
|
IconSearch,
|
||||||
|
IconUsers,
|
||||||
|
IconZoomIn,
|
||||||
|
IconZoomOut,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
import { useTransitionRouter } from 'next-view-transitions'
|
||||||
import { OrganizationChart } from 'primereact/organizationchart'
|
import { OrganizationChart } from 'primereact/organizationchart'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||||
import { useTransitionRouter } from 'next-view-transitions'
|
|
||||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
@@ -47,7 +57,6 @@ export default function Page() {
|
|||||||
ta="center"
|
ta="center"
|
||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
fz={{ base: 28, md: 36, lg: 44 }}
|
fz={{ base: 28, md: 36, lg: 44 }}
|
||||||
|
|
||||||
>
|
>
|
||||||
Struktur Organisasi PPID
|
Struktur Organisasi PPID
|
||||||
</Title>
|
</Title>
|
||||||
@@ -70,14 +79,24 @@ export default function Page() {
|
|||||||
function StrukturOrganisasiPPID() {
|
function StrukturOrganisasiPPID() {
|
||||||
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
||||||
const router = useTransitionRouter()
|
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(() => {
|
useEffect(() => {
|
||||||
void stateOrganisasi.findMany.load()
|
void stateOrganisasi.findMany.load()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
!stateOrganisasi.findMany.data &&
|
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
|
||||||
stateOrganisasi.findMany.loading !== false
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -93,10 +112,7 @@ function StrukturOrganisasiPPID() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||||
!stateOrganisasi.findMany.data ||
|
|
||||||
stateOrganisasi.findMany.data.length === 0
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Center py={40}>
|
<Center py={40}>
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
@@ -117,8 +133,7 @@ function StrukturOrganisasiPPID() {
|
|||||||
Data pegawai belum tersedia
|
Data pegawai belum tersedia
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt="xs">
|
<Text c="dimmed" mt="xs">
|
||||||
Belum ada data pegawai yang tercatat untuk PPID. Silakan coba
|
Belum ada data pegawai yang tercatat untuk PPID.
|
||||||
muat ulang atau periksa sumber data.
|
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="center" mt="lg">
|
<Group justify="center" mt="lg">
|
||||||
<Button
|
<Button
|
||||||
@@ -129,15 +144,6 @@ function StrukturOrganisasiPPID() {
|
|||||||
>
|
>
|
||||||
Muat Ulang
|
Muat Ulang
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
leftSection={<IconSearch size={16} />}
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() =>
|
|
||||||
stateOrganisasi.findMany.load({ query: { q: '' } })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Cari Pegawai
|
|
||||||
</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -145,44 +151,39 @@ function StrukturOrganisasiPPID() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buat struktur organisasi
|
||||||
const posisiMap = new Map<string, any>()
|
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) {
|
for (const pegawai of aktifPegawai) {
|
||||||
const posisiId = pegawai.posisi.id;
|
const posisiId = pegawai.posisi.id
|
||||||
if (!posisiMap.has(posisiId)) {
|
if (!posisiMap.has(posisiId)) {
|
||||||
posisiMap.set(posisiId, {
|
posisiMap.set(posisiId, {
|
||||||
...pegawai.posisi,
|
...pegawai.posisi,
|
||||||
pegawaiList: [],
|
pegawaiList: [],
|
||||||
children: [],
|
children: [],
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
|
||||||
}
|
}
|
||||||
|
|
||||||
const root: any[] = []
|
const root: any[] = []
|
||||||
posisiMap.forEach((posisi) => {
|
posisiMap.forEach((posisi) => {
|
||||||
if (posisi.parentId) {
|
if (posisi.parentId) {
|
||||||
const parent = posisiMap.get(posisi.parentId)
|
const parent = posisiMap.get(posisi.parentId)
|
||||||
if (parent) {
|
if (parent) parent.children.push(posisi)
|
||||||
parent.children.push(posisi)
|
else root.push(posisi)
|
||||||
} else {
|
} else root.push(posisi)
|
||||||
root.push(posisi)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
root.push(posisi)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function toOrgChartFormat(node: any): any {
|
function toOrgChartFormat(node: any): any {
|
||||||
const pegawai = node.pegawaiList?.[0];
|
const pegawai = node.pegawaiList?.[0]
|
||||||
return {
|
return {
|
||||||
expanded: true,
|
expanded: true,
|
||||||
type: 'person',
|
type: 'person',
|
||||||
styleClass: 'p-person',
|
styleClass: 'p-person',
|
||||||
data: {
|
data: {
|
||||||
id: pegawai?.id || null, // tambahin ini bro
|
id: pegawai?.id || null,
|
||||||
name: pegawai?.namaLengkap || 'Belum ditugaskan',
|
name: pegawai?.namaLengkap || 'Belum ditugaskan',
|
||||||
title: node.nama || 'Tanpa jabatan',
|
title: node.nama || 'Tanpa jabatan',
|
||||||
image: pegawai?.image?.link || '/img/default.png',
|
image: pegawai?.image?.link || '/img/default.png',
|
||||||
@@ -190,28 +191,112 @@ function StrukturOrganisasiPPID() {
|
|||||||
positionId: node.id || null,
|
positionId: node.id || null,
|
||||||
},
|
},
|
||||||
children: node.children?.map(toOrgChartFormat) || [],
|
children: node.children?.map(toOrgChartFormat) || [],
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Box py={16} >
|
<Stack align="center" mt="xl">
|
||||||
<Paper
|
{/* 🔍 Search + Zoom + Fullscreen controls */}
|
||||||
radius="md"
|
<Group mb="md" justify="center" gap="sm" align="center">
|
||||||
p="md"
|
<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={{
|
style={{
|
||||||
background: 'rgba(28,110,164,0.2)',
|
fontSize: 14,
|
||||||
border: `1px solid rgba(255,255,255,0.1)`,
|
fontWeight: 600,
|
||||||
overflowX: 'auto',
|
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={{
|
||||||
|
overflow: 'auto',
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
transformOrigin: 'center top',
|
||||||
|
transition: 'transform 0.25s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OrganizationChart
|
<OrganizationChart
|
||||||
value={chartData}
|
value={chartData}
|
||||||
nodeTemplate={(node) => nodeTemplate(node, router)}
|
nodeTemplate={(node) => nodeTemplate(node, router)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +306,6 @@ function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>)
|
|||||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||||
const description = node?.data?.description || ''
|
const description = node?.data?.description || ''
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition mounted transition="pop" duration={240}>
|
<Transition mounted transition="pop" duration={240}>
|
||||||
{(styles) => (
|
{(styles) => (
|
||||||
@@ -244,15 +328,15 @@ function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>)
|
|||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={name}
|
alt={name}
|
||||||
radius="md"
|
radius="md"
|
||||||
width={120}
|
width={60}
|
||||||
height={120}
|
height={60}
|
||||||
fit="cover"
|
fit="cover"
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
border: '2px solid rgba(255,255,255,0.2)',
|
border: '2px solid rgba(255,255,255,0.2)',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
}}
|
}}
|
||||||
loading='lazy'
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<Text fw={700}>{name}</Text>
|
<Text fw={700}>{name}</Text>
|
||||||
<Text size="sm" c="dimmed" mt={4}>
|
<Text size="sm" c="dimmed" mt={4}>
|
||||||
@@ -261,7 +345,6 @@ function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>)
|
|||||||
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
||||||
{description || 'Belum ada deskripsi.'}
|
{description || 'Belum ada deskripsi.'}
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip label="Lihat Detail" withArrow position="bottom">
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -273,7 +356,6 @@ function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>)
|
|||||||
>
|
>
|
||||||
Lihat Detail
|
Lihat Detail
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@@ -54,12 +54,11 @@ function Page() {
|
|||||||
ta="center"
|
ta="center"
|
||||||
fz={{ base: 28, md: 36 }}
|
fz={{ base: 28, md: 36 }}
|
||||||
fw={800}
|
fw={800}
|
||||||
variant="gradient"
|
c={colors['blue-button']}
|
||||||
gradient={{ from: colors['blue-button'], to: 'cyan', deg: 45 }}
|
|
||||||
>
|
>
|
||||||
Moto PPID Desa Darmasaba
|
Moto PPID Desa Darmasaba
|
||||||
</Text>
|
</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
|
Memberikan informasi yang cepat, mudah, tepat, dan transparan
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -67,7 +66,8 @@ function Page() {
|
|||||||
<Divider my="sm" labelPosition="center" label={<IconSparkles size={18} />} />
|
<Divider my="sm" labelPosition="center" label={<IconSparkles size={18} />} />
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text ta="center" fz={{ base: 24, md: 30 }} fw={700} mb="sm">
|
<Text ta="center" fz={{ base: 24, md: 30 }} fw={800}
|
||||||
|
c={colors['blue-button']} mb="sm">
|
||||||
Visi PPID
|
Visi PPID
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -82,13 +82,13 @@ function Page() {
|
|||||||
<Divider my="sm" />
|
<Divider my="sm" />
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text ta="center" fz={{ base: 24, md: 30 }} fw={700} mb="sm">
|
<Text ta="center" fz={{ base: 24, md: 30 }} fw={800}
|
||||||
|
c={colors['blue-button']} mb="sm">
|
||||||
Misi PPID
|
Misi PPID
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fz={{ base: 'md', md: 'lg' }}
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
lh={1.7}
|
lh={1.7}
|
||||||
ta="center"
|
|
||||||
dangerouslySetInnerHTML={{ __html: item.misi }}
|
dangerouslySetInnerHTML={{ __html: item.misi }}
|
||||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
|
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import { Carousel, CarouselSlide } from "@mantine/carousel";
|
import { Carousel } from "@mantine/carousel";
|
||||||
import { Box, Button, Container, Group, Paper, Stack, Text, useMantineTheme, Skeleton } from "@mantine/core";
|
import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text, useMantineTheme } from "@mantine/core";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
import { IconArrowRight, IconAward } from "@tabler/icons-react";
|
||||||
import Autoplay from "embla-carousel-autoplay";
|
import Autoplay from "embla-carousel-autoplay";
|
||||||
import { IconAward, IconArrowRight } from "@tabler/icons-react";
|
|
||||||
import { useTransitionRouter } from "next-view-transitions";
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
@@ -18,7 +18,8 @@ export default function Page() {
|
|||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Container w={{ base: "100%", md: "60%" }}>
|
<Container w={{ base: "100%", md: "90%", lg: "60%" }}>
|
||||||
|
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconAward size={40} color={colors["blue-button"]} />
|
<IconAward size={40} color={colors["blue-button"]} />
|
||||||
@@ -37,11 +38,10 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Slider() {
|
function Slider() {
|
||||||
const height = 500;
|
|
||||||
const width = 1200;
|
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
|
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 state = useProxy(penghargaanState);
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ function Slider() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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" />
|
||||||
<Skeleton w={300} h={200} radius="lg" visibleFrom="sm" />
|
<Skeleton w={300} h={200} radius="lg" visibleFrom="sm" />
|
||||||
<Skeleton w={300} h={200} radius="lg" visibleFrom="md" />
|
<Skeleton w={300} h={200} radius="lg" visibleFrom="md" />
|
||||||
@@ -74,31 +74,49 @@ function Slider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slides = data.map((item) => (
|
const slides = data.map((item) => (
|
||||||
<CarouselSlide key={item.id}>
|
<Carousel.Slide key={item.id}>
|
||||||
<Paper
|
<Paper
|
||||||
h="100%"
|
|
||||||
radius="lg"
|
radius="lg"
|
||||||
shadow="md"
|
shadow="md"
|
||||||
pos="relative"
|
pos="relative"
|
||||||
style={{
|
style={{
|
||||||
|
height: "100%",
|
||||||
backgroundImage: `url(${item.image?.link})`,
|
backgroundImage: `url(${item.image?.link})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
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
|
<Box
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
inset={0}
|
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 }}
|
style={{ borderRadius: 16 }}
|
||||||
/>
|
/>
|
||||||
<Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative">
|
<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}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push(`/darmasaba/penghargaan/${item.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/penghargaan/${item.id}`)
|
||||||
|
}
|
||||||
size="md"
|
size="md"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
rightSection={<IconArrowRight size={18} />}
|
rightSection={<IconArrowRight size={18} />}
|
||||||
@@ -110,24 +128,83 @@ function Slider() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</CarouselSlide>
|
</Carousel.Slide>
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
w="100%"
|
||||||
|
mx="auto"
|
||||||
|
px={{ base: "md", sm: "xl", md: "2rem", lg: "3rem" }}
|
||||||
|
style={{
|
||||||
|
maxWidth: 1300,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Carousel
|
<Carousel
|
||||||
py="xl"
|
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]}
|
plugins={[autoplay.current]}
|
||||||
onMouseEnter={autoplay.current.stop}
|
onMouseEnter={autoplay.current.stop}
|
||||||
onMouseLeave={autoplay.current.reset}
|
onMouseLeave={autoplay.current.reset}
|
||||||
w={{ base: "100%", sm: "90%", md: "80%", lg: width }}
|
withControls={data.length > 3}
|
||||||
h={height}
|
draggable={data.length > 1}
|
||||||
slideSize={{ base: "100%", sm: "50%", md: "33.333333%" }}
|
styles={{
|
||||||
slideGap="md"
|
root: {
|
||||||
loop
|
position: "relative",
|
||||||
align="start"
|
},
|
||||||
slidesToScroll={mobile ? 1 : 2}
|
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}
|
{slides}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,100 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
|
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() {
|
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 (
|
return (
|
||||||
<Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white">
|
<Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white">
|
||||||
<Box w="100%" p="xl">
|
<Box w="100%" p="xl">
|
||||||
@@ -64,31 +156,39 @@ function Footer() {
|
|||||||
Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||||
</Text>
|
</Text>
|
||||||
<Flex gap="md" mt="sm" c="#F3F2EC">
|
<Flex gap="md" mt="sm" c="#F3F2EC">
|
||||||
<ActionIcon variant="subtle" color="white"><IconBrandFacebook size={22} /></ActionIcon>
|
{sosialMedia.map((item) => (
|
||||||
<ActionIcon variant="subtle" color="white"><IconBrandInstagram size={22} /></ActionIcon>
|
<ActionIcon
|
||||||
<ActionIcon variant="subtle" color="white"><IconBrandTwitter size={22} /></ActionIcon>
|
key={item.title}
|
||||||
<ActionIcon variant="subtle" color="white"><IconBrandWhatsapp size={22} /></ActionIcon>
|
component="a"
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="subtle"
|
||||||
|
color="white"
|
||||||
|
>
|
||||||
|
<item.icon size={22} />
|
||||||
|
</ActionIcon>
|
||||||
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text c="white" fz="md" fw={700}>Layanan Desa</Text>
|
<Text c="white" fz="md" fw={700}>Layanan Desa</Text>
|
||||||
<Anchor c="#F3F2EC" fz="xs">Administrasi Kependudukan</Anchor>
|
{layanandesa.map((item) => (
|
||||||
<Anchor c="#F3F2EC" fz="xs">Layanan Sosial</Anchor>
|
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
|
||||||
<Anchor c="#F3F2EC" fz="xs">Pengaduan Masyarakat</Anchor>
|
))}
|
||||||
<Anchor c="#F3F2EC" fz="xs">Informasi Publik</Anchor>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text c="white" fz="md" fw={700}>Tautan Penting</Text>
|
<Text c="white" fz="md" fw={700}>Tautan Penting</Text>
|
||||||
<Anchor c="#F3F2EC" fz="xs">Portal Badung</Anchor>
|
{tautanPenting.map((item) => (
|
||||||
<Anchor c="#F3F2EC" fz="xs">E-Government</Anchor>
|
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
|
||||||
<Anchor c="#F3F2EC" fz="xs">Transparansi</Anchor>
|
))}
|
||||||
<Anchor c="#F3F2EC" fz="xs">Unduhan</Anchor>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -101,8 +201,9 @@ function Footer() {
|
|||||||
w="70%"
|
w="70%"
|
||||||
placeholder="Masukkan email Anda"
|
placeholder="Masukkan email Anda"
|
||||||
rightSection={<IconAt size={16} />}
|
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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,24 +7,38 @@ import GlobalSearch from "./globalSearch";
|
|||||||
export function NavbarSearch() {
|
export function NavbarSearch() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isNavigatingRef = useRef(false);
|
||||||
|
|
||||||
// Close when clicking outside
|
// Close when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
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);
|
setIsOpen(false);
|
||||||
stateNav.clear();
|
stateNav.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
// Clean up
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Reset navigation flag saat component unmount atau route change
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isNavigatingRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
@@ -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 { ActionIcon, Box, Burger, Group, Image, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
import { IconSquareArrowRight } from "@tabler/icons-react";
|
import { IconSquareArrowRight } from "@tabler/icons-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import { MenuItem } from "../../../../types/menu-item";
|
import { MenuItem } from "../../../../types/menu-item";
|
||||||
import { NavbarMainMenu } from "./NavbarMainMenu";
|
import { NavbarMainMenu } from "./NavbarMainMenu";
|
||||||
@@ -19,14 +19,18 @@ export function Navbar() {
|
|||||||
<Paper
|
<Paper
|
||||||
radius="0"
|
radius="0"
|
||||||
className="glass2"
|
className="glass2"
|
||||||
w="100%"
|
w="100vw"
|
||||||
pos="fixed"
|
pos="fixed"
|
||||||
top={0}
|
top={0}
|
||||||
style={{ zIndex: 100 }}
|
style={{ zIndex: 100 }}
|
||||||
>
|
>
|
||||||
|
{/* Desktop navbar (muncul mulai 992px ke atas) */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<NavbarMainMenu listNavbar={navbarListMenu} />
|
<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">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
@@ -37,11 +41,22 @@ export function Navbar() {
|
|||||||
stateNav.mobileOpen = false;
|
stateNav.mobileOpen = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip label="Go to homepage" position="bottom" withArrow>
|
<Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
|
||||||
<Image src="/darmasaba-icon.png" alt="Village Logo" width={48} height={48} loading="lazy"/>
|
<Image
|
||||||
|
src="/darmasaba-icon.png"
|
||||||
|
alt="Village Logo"
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Tooltip label={mobileOpen ? "Close menu" : "Open menu"} position="bottom" withArrow>
|
|
||||||
|
<Tooltip
|
||||||
|
label={mobileOpen ? "Close menu" : "Open menu"}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
<Burger
|
<Burger
|
||||||
opened={mobileOpen}
|
opened={mobileOpen}
|
||||||
color={colors["blue-button"]}
|
color={colors["blue-button"]}
|
||||||
@@ -50,12 +65,14 @@ export function Navbar() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<Paper
|
<Paper
|
||||||
component={motion.div}
|
component={motion.div}
|
||||||
initial={{ x: '100%' }}
|
bg="white"
|
||||||
|
initial={{ x: "100%" }}
|
||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: '100%' }}
|
exit={{ x: "100%" }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
left={0}
|
left={0}
|
||||||
@@ -63,12 +80,14 @@ export function Navbar() {
|
|||||||
top="100%"
|
top="100%"
|
||||||
m={0}
|
m={0}
|
||||||
radius={0}
|
radius={0}
|
||||||
|
shadow="md"
|
||||||
>
|
>
|
||||||
<NavbarMobile listNavbar={navbarListMenu} />
|
<NavbarMobile listNavbar={navbarListMenu} />
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{(item || isSearch) && <Box className="glass" />}
|
{(item || isSearch) && <Box className="glass" />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -76,35 +95,105 @@ export function Navbar() {
|
|||||||
|
|
||||||
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||||
const router = useRouter();
|
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(100dvh - 80px)"
|
||||||
|
type="auto"
|
||||||
|
offsetScrollbars
|
||||||
|
>
|
||||||
|
<Stack p="sm" gap="xs">
|
||||||
|
{listNavbar.map((item, k) => {
|
||||||
|
const active = isActive(item.href);
|
||||||
return (
|
return (
|
||||||
<ScrollArea.Autosize mah="calc(100vh - 80px)" offsetScrollbars>
|
|
||||||
<Stack p="md" gap="xs">
|
|
||||||
{listNavbar.map((item, k) => (
|
|
||||||
<Box key={k}>
|
<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
|
<Group
|
||||||
|
key={j}
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
align="center"
|
align="center"
|
||||||
p="xs"
|
p="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(item.href);
|
if (child.href) {
|
||||||
|
router.push(child.href);
|
||||||
stateNav.mobileOpen = false;
|
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",
|
||||||
}}
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
>
|
||||||
<Text c="dark.9" fw={600} fz="md">
|
<Text
|
||||||
{item.name}
|
fz="sm"
|
||||||
|
fw={childActive ? 600 : 400}
|
||||||
|
c={childActive ? "blue.7" : "dark.8"}
|
||||||
|
>
|
||||||
|
{child.name}
|
||||||
</Text>
|
</Text>
|
||||||
<IconSquareArrowRight size={18} />
|
<IconSquareArrowRight
|
||||||
|
size={14}
|
||||||
|
color={childActive ? "#1e66f5" : "inherit"}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
{item.children && (
|
);
|
||||||
<Box pl="md">
|
})}
|
||||||
<NavbarMobile listNavbar={item.children} />
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea.Autosize>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
import colors from "@/con/colors"
|
import colors from "@/con/colors"
|
||||||
import stateNav from "@/state/state-nav"
|
import stateNav from "@/state/state-nav"
|
||||||
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core"
|
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
|
||||||
import { useHover } from "@mantine/hooks"
|
|
||||||
import { IconSearch, IconUser } from "@tabler/icons-react"
|
import { IconSearch, IconUser } from "@tabler/icons-react"
|
||||||
import { useTransitionRouter } from 'next-view-transitions'
|
import { useTransitionRouter } from 'next-view-transitions'
|
||||||
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
import { useSnapshot } from "valtio"
|
import { useSnapshot } from "valtio"
|
||||||
import { MenuItem } from "../../../../types/menu-item"
|
import { MenuItem } from "../../../../types/menu-item"
|
||||||
import { NavbarSearch } from "./NavBarSearch"
|
import { NavbarSearch } from "./NavBarSearch"
|
||||||
import { NavbarSubMenu } from "./NavbarSubMenu"
|
import { NavbarSubMenu } from "./NavbarSubMenu"
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
|
|
||||||
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
|
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
|
||||||
const stateAuth = {
|
const stateAuth = {
|
||||||
@@ -21,12 +20,13 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
const { item, isSearch } = useSnapshot(stateNav)
|
const { item, isSearch } = useSnapshot(stateNav)
|
||||||
const router = useTransitionRouter()
|
const router = useTransitionRouter()
|
||||||
const next = useRouter()
|
const next = useRouter()
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
|
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
|
||||||
<Container pos="relative" w={{ base: '100%', md: '80%' }} fluid>
|
<Container pos="relative" w={{ base: '100%', md: '80%' }} fluid>
|
||||||
<Flex align="center" justify="space-between" wrap={{ base: "wrap", md: "nowrap" }}>
|
<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
|
<ActionIcon
|
||||||
radius="xl"
|
radius="xl"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
@@ -47,10 +47,15 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{listNavbar.map((item, k) => (
|
{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
|
<ActionIcon
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
c={isSearch ? 'gray' : colors["blue-button"]}
|
c={isSearch ? 'gray' : colors["blue-button"]}
|
||||||
@@ -66,7 +71,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
|
|
||||||
{/* hanya tampil kalau role = admin */}
|
{/* hanya tampil kalau role = admin */}
|
||||||
{stateAuth.role === "admin" && (
|
{stateAuth.role === "admin" && (
|
||||||
<Tooltip label="My Profile" position="bottom" withArrow>
|
<Tooltip label="Profil Saya" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
next.push("/admin/landing-page/profile/program-inovasi")
|
next.push("/admin/landing-page/profile/program-inovasi")
|
||||||
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItemCom({ item }: { item: MenuItem }) {
|
function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
|
||||||
const { ref, hovered } = useHover()
|
|
||||||
const router = useTransitionRouter()
|
const router = useTransitionRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Menu
|
||||||
|
trigger="hover"
|
||||||
|
position="bottom-start"
|
||||||
|
offset={20}
|
||||||
|
width={300}
|
||||||
|
shadow="md"
|
||||||
|
withinPortal
|
||||||
|
onOpen={() => {
|
||||||
|
stateNav.item = item.children || null;
|
||||||
|
stateNav.isSearch = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuTarget>
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
|
||||||
color={hovered ? "gray" : colors["blue-button"]}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
stateNav.item = item.children || null
|
|
||||||
stateNav.isSearch = false
|
|
||||||
}}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="xl"
|
color={isActive ? 'blue' : 'gray'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(item.href)
|
if (item.href) {
|
||||||
stateNav.clear()
|
router.push(item.href);
|
||||||
|
stateNav.clear();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
fontWeight: isActive ? 600 : 400,
|
||||||
|
borderBottom: isActive ? `2px solid ${colors['blue-button']}` : 'none',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
fw={500}
|
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Button>
|
</Button>
|
||||||
|
</MenuTarget>
|
||||||
|
</Menu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react";
|
|||||||
import { MenuItem } from "../../../../types/menu-item";
|
import { MenuItem } from "../../../../types/menu-item";
|
||||||
import { useTransitionRouter } from "next-view-transitions";
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={Math.random().toString(36).slice(2)}
|
key={Math.random().toString(36).slice(2)}
|
||||||
@@ -37,23 +38,24 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
|||||||
justify="space-between"
|
justify="space-between"
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
color="gray.0"
|
color={link.href && pathname.startsWith(link.href) ? 'blue' : 'gray'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (link.href) {
|
||||||
router.push(link.href);
|
router.push(link.href);
|
||||||
stateNav.item = null;
|
stateNav.item = null;
|
||||||
stateNav.isSearch = false;
|
stateNav.isSearch = false;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
rightSection={<IconArrowRight size={18} />}
|
rightSection={<IconArrowRight size={18} />}
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
root: {
|
root: {
|
||||||
background: "transparent",
|
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent',
|
||||||
color: colors['blue-button'],
|
color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'],
|
||||||
fontWeight: 500,
|
fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500,
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
background: theme.colors.gray[8],
|
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[1] : theme.colors.gray[0],
|
||||||
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,91 +1,184 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
|
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
|
||||||
import { Box, Center, Loader, Stack, Text, TextInput } from '@mantine/core';
|
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
|
||||||
import { IconX } from '@tabler/icons-react';
|
import { IconX } from '@tabler/icons-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
import getDetailUrl from './searchUrl';
|
import getDetailUrl from './searchUrl';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function GlobalSearch() {
|
export default function GlobalSearch() {
|
||||||
const snap = useSnapshot(searchState);
|
const snap = useSnapshot(searchState);
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const [isNavigating, setIsNavigating] = useState(false);
|
||||||
|
|
||||||
// Infinite scroll
|
// Buka popover saat ada query
|
||||||
|
useEffect(() => {
|
||||||
|
setOpened(!!snap.query);
|
||||||
|
}, [snap.query]);
|
||||||
|
|
||||||
|
// Infinite scroll handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const bottom =
|
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
|
||||||
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
|
if (nearBottom && !snap.loading) searchState.next();
|
||||||
if (bottom && !snap.loading) searchState.next();
|
|
||||||
};
|
};
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
}, [snap.loading]);
|
}, [snap.loading]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchState.query = '';
|
||||||
|
searchState.results = [];
|
||||||
|
searchState.page = 1;
|
||||||
|
searchState.nextPage = null;
|
||||||
|
setOpened(false);
|
||||||
|
setIsNavigating(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maw={800} mx="auto">
|
<Box pos="relative">
|
||||||
{/* 🔍 Search input */}
|
<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
|
<TextInput
|
||||||
placeholder="Cari apapun..."
|
placeholder="Cari apapun..."
|
||||||
value={snap.query}
|
value={snap.query}
|
||||||
onChange={(e) => (
|
onChange={(e) => {
|
||||||
searchState.query = e.currentTarget.value,
|
searchState.query = e.currentTarget.value;
|
||||||
debouncedFetch()
|
debouncedFetch();
|
||||||
)}
|
}}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
size="md"
|
||||||
rightSection={
|
rightSection={
|
||||||
snap.query ? (
|
snap.query ? (
|
||||||
<IconX
|
<IconX
|
||||||
size={16}
|
size={16}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={clearSearch}
|
||||||
searchState.query = '';
|
|
||||||
searchState.results = [];
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Popover.Target>
|
||||||
|
|
||||||
{/* 📄 Hasil pencarian */}
|
<Popover.Dropdown
|
||||||
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
p={0}
|
||||||
{snap.results.map((item, i) => (
|
style={{
|
||||||
|
maxHeight: 350,
|
||||||
|
overflowY: 'auto',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #eee',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[...snap.results].length > 0 ? (
|
||||||
|
[...snap.results].map((item: any, i: number) => (
|
||||||
<Box
|
<Box
|
||||||
key={i}
|
key={i}
|
||||||
p="sm"
|
p="sm"
|
||||||
|
className="search-result-item" // Add class untuk prevent close
|
||||||
style={{
|
style={{
|
||||||
borderBottom: '1px solid #eee',
|
borderBottom: '1px solid #f1f1f1',
|
||||||
cursor: 'pointer',
|
cursor: isNavigating ? 'wait' : 'pointer',
|
||||||
|
background: 'white',
|
||||||
transition: 'background 0.2s',
|
transition: 'background 0.2s',
|
||||||
overflow: 'hidden',
|
opacity: isNavigating ? 0.6 : 1,
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
maxWidth: '100%'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
|
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
|
||||||
onClick={() => {
|
|
||||||
const url = getDetailUrl(item);
|
|
||||||
window.location.href = url;
|
|
||||||
}}
|
}}
|
||||||
|
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}>
|
<Text size="sm" fw={500} lineClamp={1}>
|
||||||
{item.judul || item.namaPasar || item.nama || item.name}
|
{item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||||
dari modul: {item.type}
|
dari modul: {item.type || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))
|
||||||
</div>
|
) : (
|
||||||
|
|
||||||
{/* ⏳ Loader di bawah hasil */}
|
|
||||||
{snap.loading && (
|
|
||||||
<Center py="md">
|
<Center py="md">
|
||||||
<Loader size="sm" />
|
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ function Apbdes() {
|
|||||||
const data = (state.findMany.data || []).slice(0, 3)
|
const data = (state.findMany.data || []).slice(0, 3)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="lg" gap="4rem" bg={colors.Bg}>
|
<Stack p="sm" gap="xl" bg={colors.Bg}>
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="sm">
|
<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}
|
{textHeading.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta={"center"} fz={{ base: '1rem', sm: '1.3rem' }} c="dimmed">
|
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||||
{textHeading.des}
|
{textHeading.des}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -117,7 +117,7 @@ function Apbdes() {
|
|||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Group pb={80} justify="center">
|
<Group justify="center" pb={10}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
href="/darmasaba/apbdes"
|
href="/darmasaba/apbdes"
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ function DesaAntiKorupsi() {
|
|||||||
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
|
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
|
||||||
return (
|
return (
|
||||||
<Stack gap={"0"} bg={colors.Bg} p={"sm"}>
|
<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>
|
<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>
|
</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>
|
<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}>
|
<Center py={20}>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function Kepuasan() {
|
|||||||
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
||||||
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
||||||
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = 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 [opened, { open, close }] = useDisclosure(false)
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -121,18 +121,18 @@ function Kepuasan() {
|
|||||||
|
|
||||||
// Convert map to array and sort by date
|
// Convert map to array and sort by date
|
||||||
const barData = Array.from(monthYearMap.entries())
|
const barData = Array.from(monthYearMap.entries())
|
||||||
.map(([key, count]) => {
|
.map(([key, Responden]) => {
|
||||||
const [year, month] = key.split('-');
|
const [year, month] = key.split('-');
|
||||||
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
||||||
.toLocaleString('id-ID', { month: 'long' });
|
.toLocaleString('id-ID', { month: 'long' });
|
||||||
return {
|
return {
|
||||||
month: `${monthName} ${year}`,
|
month: `${monthName} ${year}`,
|
||||||
count,
|
Responden,
|
||||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.sortKey - b.sortKey)
|
.sort((a, b) => a.sortKey - b.sortKey)
|
||||||
.map(({ month, count }) => ({ month, count }));
|
.map(({ month, Responden }) => ({ month, Responden }));
|
||||||
|
|
||||||
setBarChartData(barData);
|
setBarChartData(barData);
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ function Kepuasan() {
|
|||||||
|
|
||||||
if ((loading && !data) || !data) {
|
if ((loading && !data) || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10} px="xl">
|
<Stack py={10} px="sm">
|
||||||
<Skeleton height={300} mb="md" />
|
<Skeleton height={300} mb="md" />
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={300} />
|
||||||
@@ -154,11 +154,17 @@ function Kepuasan() {
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Stack p="sm">
|
<Stack p="sm">
|
||||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
<Container w={{ base: "100%", md: "80%" }} p={"sm"}>
|
||||||
<Center>
|
<Center>
|
||||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: '2rem', md: '2.8rem' }}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fw={800}
|
||||||
|
style={{ letterSpacing: '-0.5px' }}
|
||||||
|
>Indeks Kepuasan Masyarakat</Text>
|
||||||
</Center>
|
</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}>
|
<Center mt={10}>
|
||||||
<Button
|
<Button
|
||||||
radius={"lg"}
|
radius={"lg"}
|
||||||
@@ -168,7 +174,7 @@ function Kepuasan() {
|
|||||||
>Ajukan Responden</Button>
|
>Ajukan Responden</Button>
|
||||||
</Center>
|
</Center>
|
||||||
</Container>
|
</Container>
|
||||||
<Box px={"xl"}>
|
<Box px={"sm"}>
|
||||||
<Paper p={"lg"} bg={colors.Bg}>
|
<Paper p={"lg"} bg={colors.Bg}>
|
||||||
<Paper p={"lg"}>
|
<Paper p={"lg"}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
@@ -185,7 +191,7 @@ function Kepuasan() {
|
|||||||
h={window.innerWidth < 480 ? 200 : 300}
|
h={window.innerWidth < 480 ? 200 : 300}
|
||||||
data={barChartData}
|
data={barChartData}
|
||||||
dataKey="month"
|
dataKey="month"
|
||||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||||
tickLine="y"
|
tickLine="y"
|
||||||
xAxisLabel="Bulan"
|
xAxisLabel="Bulan"
|
||||||
yAxisLabel="Jumlah Responden"
|
yAxisLabel="Jumlah Responden"
|
||||||
@@ -332,7 +338,7 @@ function Kepuasan() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
label="Nama"
|
label="Nama"
|
||||||
type='text'
|
type='text'
|
||||||
placeholder="masukkan nama"
|
placeholder="Masukkan nama"
|
||||||
defaultValue={state.create.form.name}
|
defaultValue={state.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
state.create.form.name = val.currentTarget.value;
|
state.create.form.name = val.currentTarget.value;
|
||||||
@@ -416,16 +422,22 @@ function Kepuasan() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack p={"sm"}>
|
<Stack p={"sm"}>
|
||||||
<Container size="lg" px="md">
|
<Container size="lg" px="sm">
|
||||||
<Center>
|
<Center>
|
||||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: '2rem', md: '2.8rem' }}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fw={800}
|
||||||
|
style={{ letterSpacing: '-0.5px' }}
|
||||||
|
>Indeks Kepuasan Masyarakat</Text>
|
||||||
</Center>
|
</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 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}>
|
<Center mt={10}>
|
||||||
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||||
</Center>
|
</Center>
|
||||||
</Container>
|
</Container>
|
||||||
<Box px={"xl"}>
|
<Box px={"md"}>
|
||||||
<Paper p={"lg"} bg={colors.Bg}>
|
<Paper p={"lg"} bg={colors.Bg}>
|
||||||
<Paper p={"lg"}>
|
<Paper p={"lg"}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
@@ -448,7 +460,7 @@ function Kepuasan() {
|
|||||||
h={300}
|
h={300}
|
||||||
data={barChartData}
|
data={barChartData}
|
||||||
dataKey="month"
|
dataKey="month"
|
||||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
series={[{ name: 'Responden', color: colors['blue-button'] }]}
|
||||||
tickLine="y"
|
tickLine="y"
|
||||||
xAxisLabel="Bulan"
|
xAxisLabel="Bulan"
|
||||||
yAxisLabel="Jumlah Responden"
|
yAxisLabel="Jumlah Responden"
|
||||||
@@ -605,7 +617,7 @@ function Kepuasan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Tanggal"
|
label="Tanggal Pengisian"
|
||||||
type="date"
|
type="date"
|
||||||
placeholder="masukkan tanggal"
|
placeholder="masukkan tanggal"
|
||||||
defaultValue={state.create.form.tanggal}
|
defaultValue={state.create.form.tanggal}
|
||||||
|
|||||||
@@ -6,20 +6,19 @@ import {
|
|||||||
Center,
|
Center,
|
||||||
Image,
|
Image,
|
||||||
Paper,
|
Paper,
|
||||||
|
ScrollArea,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
useMantineColorScheme
|
||||||
Skeleton,
|
|
||||||
useMantineColorScheme,
|
|
||||||
ScrollArea,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { IconPhotoOff } from "@tabler/icons-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useTransitionRouter } from "next-view-transitions";
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { IconPhotoOff } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
|
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div whileHover={{ scale: 1.03 }}>
|
<motion.div whileHover={{ scale: 1.03 }}>
|
||||||
<Tooltip label={`Lihat ${data.name}`} withArrow>
|
|
||||||
<Paper
|
<Paper
|
||||||
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
|
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
|
||||||
p="lg"
|
p="lg"
|
||||||
@@ -47,10 +45,10 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
src={data.image.link}
|
src={data.image.link}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
radius="md"
|
radius="md"
|
||||||
fit="cover"
|
fit="contain"
|
||||||
h={140}
|
h={140}
|
||||||
w="100%"
|
w="100%"
|
||||||
loading="lazy"
|
style={{ objectPosition: "center" }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
@@ -67,7 +65,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Tooltip>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,41 @@ export default function ProfileView({ data }: ProfileViewProps) {
|
|||||||
justify="end"
|
justify="end"
|
||||||
align="end"
|
align="end"
|
||||||
pos="relative"
|
pos="relative"
|
||||||
w={{ base: '100%', md: '40%' }}
|
w={{
|
||||||
px="xl"
|
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 ? (
|
{data.image?.link ? (
|
||||||
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={data.image.link}
|
src={data.image.link}
|
||||||
alt={data.name || 'Foto profil'}
|
alt={data.name || 'Foto profil'}
|
||||||
fit="cover"
|
fit="contain"
|
||||||
radius="lg"
|
radius="lg"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
style={{
|
||||||
|
objectPosition: 'center bottom',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Stack align="center" gap="xs" w="100%" py="xl">
|
<Stack align="center" gap="xs" w="100%" py="xl">
|
||||||
<IconUserCircle size={96} stroke={1.5} />
|
<IconUserCircle size={96} stroke={1.5} />
|
||||||
@@ -49,20 +73,44 @@ export default function ProfileView({ data }: ProfileViewProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</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
|
<Card
|
||||||
px="lg"
|
px={{ base: 'md', sm: 'lg' }}
|
||||||
radius="2xl"
|
py={{ base: 'xs', sm: 'sm' }}
|
||||||
|
radius="lg"
|
||||||
withBorder
|
withBorder
|
||||||
className="glass3"
|
style={{
|
||||||
style={{ border: '1px solid rgba(255,255,255,0.15)' }}
|
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>
|
<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'}
|
{data.position || 'Tidak ada jabatan'}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</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}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Skeleton,
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
GridCol,
|
GridCol,
|
||||||
|
Group,
|
||||||
Image,
|
Image,
|
||||||
Paper,
|
Paper,
|
||||||
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Center,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
|
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ModuleView from "./ModuleView";
|
import ModuleView from "./ModuleView";
|
||||||
import SosmedView from "./SosmedView";
|
|
||||||
import ProfileView from "./ProfileView";
|
import ProfileView from "./ProfileView";
|
||||||
|
import SosmedView from "./SosmedView";
|
||||||
|
|
||||||
const getDayOfWeek = () => {
|
const getDayOfWeek = () => {
|
||||||
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
|
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
|
||||||
@@ -120,23 +121,21 @@ function LandingPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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" }}>
|
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}>
|
||||||
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
|
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
|
||||||
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
|
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Flex gap="md" wrap="wrap">
|
<Flex gap="md" wrap="wrap">
|
||||||
<Grid w="100%">
|
<Group>
|
||||||
<Grid.Col span={{ base: 3, sm: 2 }}>
|
|
||||||
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
||||||
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
|
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={{ base: 9, sm: 10 }}>
|
|
||||||
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
|
||||||
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
|
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid.Col>
|
</Group>
|
||||||
|
<Grid w="100%">
|
||||||
<Grid.Col span={12}>
|
<Grid.Col span={12}>
|
||||||
<Paper
|
<Paper
|
||||||
bg={colors["blue-button"]}
|
bg={colors["blue-button"]}
|
||||||
|
|||||||
@@ -30,17 +30,13 @@ const textHeading = {
|
|||||||
function Layanan() {
|
function Layanan() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.grey[1]} gap={"42"} py={"xl"}>
|
<Stack pos={"relative"} bg={colors.grey[1]} gap={"xl"} py={"md"}>
|
||||||
<Container w={{ base: "100%", md: "50%" }} p={"xl"}>
|
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
|
||||||
<Stack align="center" gap={"0"}>
|
<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}
|
{textHeading.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||||
style={{
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{textHeading.des}
|
{textHeading.des}
|
||||||
</Text>
|
</Text>
|
||||||
<Box p={"md"}>
|
<Box p={"md"}>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
BackgroundImage,
|
BackgroundImage,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
@@ -48,15 +49,15 @@ function Potensi() {
|
|||||||
const data = (state.findMany.data || []).slice(0, 4);
|
const data = (state.findMany.data || []).slice(0, 4);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="sm" gap="4rem">
|
<Stack p="sm" gap="xl">
|
||||||
<Box>
|
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
|
||||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }} fw={700} c={colors["blue-button"]}>
|
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>
|
||||||
{textHeading.title}
|
{textHeading.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta={"center"} fz={{ base: "1.4rem", md: "1.6rem" }} c="black">
|
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||||
{textHeading.des}
|
{textHeading.des}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Container>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Stack align="center" justify="center" h={300}>
|
<Stack align="center" justify="center" h={300}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function Prestasi() {
|
|||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconTrophy size={36} color={colors["blue-button"]} />
|
<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
|
Prestasi Desa
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -55,7 +55,7 @@ function Prestasi() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Box py={50}>
|
<Box py="lg">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Center mih={200}>
|
<Center mih={200}>
|
||||||
<Loader color={colors["blue-button"]} size="xl" />
|
<Loader color={colors["blue-button"]} size="xl" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMediaQuery } from "@mantine/hooks"
|
|||||||
import { Prisma } from "@prisma/client"
|
import { Prisma } from "@prisma/client"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { IconMoodSad } from "@tabler/icons-react"
|
import { IconMoodSad } from "@tabler/icons-react"
|
||||||
|
import colors from "@/con/colors"
|
||||||
|
|
||||||
export default function SDGS() {
|
export default function SDGS() {
|
||||||
const theme = useMantineTheme()
|
const theme = useMantineTheme()
|
||||||
@@ -41,20 +42,16 @@ export default function SDGS() {
|
|||||||
order={1}
|
order={1}
|
||||||
fz={{ base: "2.4rem", md: "3.6rem" }}
|
fz={{ base: "2.4rem", md: "3.6rem" }}
|
||||||
fw={900}
|
fw={900}
|
||||||
style={{
|
c={colors["blue-button"]}
|
||||||
background: "linear-gradient(90deg, #1A5F7A, #159895)",
|
|
||||||
WebkitBackgroundClip: "text",
|
|
||||||
WebkitTextFillColor: "transparent",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
SDGs Desa
|
SDGs Desa
|
||||||
</Title>
|
</Title>
|
||||||
</Center>
|
</Center>
|
||||||
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
|
<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.
|
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>
|
</Text>
|
||||||
|
|
||||||
<Box py={50}>
|
<Box py="lg">
|
||||||
<Paper
|
<Paper
|
||||||
p={{ base: "md", md: "xl" }}
|
p={{ base: "md", md: "xl" }}
|
||||||
radius="2xl"
|
radius="2xl"
|
||||||
@@ -78,6 +75,9 @@ export default function SDGS() {
|
|||||||
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
|
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
|
||||||
border: "1px solid rgba(0,0,0,0.05)",
|
border: "1px solid rgba(0,0,0,0.05)",
|
||||||
transition: "all 0.3s ease",
|
transition: "all 0.3s ease",
|
||||||
|
height: "100%", // biar tinggi antar card konsisten
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Center mb="lg">
|
<Center mb="lg">
|
||||||
@@ -105,11 +105,21 @@ export default function SDGS() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
|
{/* Stack isi teks & angka */}
|
||||||
|
<Stack justify="space-between" align="center" gap="xs" h="100%">
|
||||||
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
|
<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}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Title
|
<Title
|
||||||
order={2}
|
order={2}
|
||||||
ta="center"
|
ta="center"
|
||||||
@@ -122,9 +132,11 @@ export default function SDGS() {
|
|||||||
>
|
>
|
||||||
{item.jumlah}
|
{item.jumlah}
|
||||||
</Title>
|
</Title>
|
||||||
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<Center mih={200} style={{ flexDirection: "column" }}>
|
<Center mih={200} style={{ flexDirection: "column" }}>
|
||||||
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
|
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
|
||||||
@@ -141,7 +153,7 @@ export default function SDGS() {
|
|||||||
href="/darmasaba/sdgs-desa"
|
href="/darmasaba/sdgs-desa"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="lg"
|
size="lg"
|
||||||
mt={40}
|
mt="md"
|
||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: "#26667F", to: "#124170" }}
|
gradient={{ from: "#26667F", to: "#124170" }}
|
||||||
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}}
|
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}}
|
||||||
|
|||||||
@@ -1,60 +1,92 @@
|
|||||||
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
||||||
const { type, id, kategori } = item;
|
const { type, id, kategori } = item;
|
||||||
const typeUrlMap: Record<string, string> = {
|
const map: Record<string, (id: string | number, kategori?: string) => string> = {
|
||||||
programinovasi: `/darmasaba/program-inovasi/${id}`,
|
programinovasi: (id) => `/darmasaba/program-inovasi/${id}`,
|
||||||
desaantikorupsi: '/darmasaba/desa-anti-korupsi',
|
desaantikorupsi: () => '/darmasaba/desa-anti-korupsi',
|
||||||
sdgsdesa: '/darmasaba/sdgs-desa',
|
sdgsdesa: () => '/darmasaba/sdgs-desa',
|
||||||
apbdes: '/darmasaba/apbdes',
|
apbdes: () => '/darmasaba/apbdes',
|
||||||
prestasidesa: '/darmasaba/prestasi-desa',
|
prestasidesa: () => '/darmasaba/prestasi-desa',
|
||||||
pejabatdesa: '/darmasaba/profile/pejabat-desa',
|
pejabatdesa: () => '/darmasaba/ppid/profile-ppid',
|
||||||
strukturppid: '/darmasaba/ppid/struktur-ppid',
|
strukturppid: () => '/darmasaba/ppid/struktur-ppid',
|
||||||
visimisippid: '/darmasaba/ppid/visi-misi',
|
visimisippid: () => '/darmasaba/ppid/visi-misi',
|
||||||
dasarhukumppid: '/darmasaba/ppid/dasar-hukum',
|
dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum',
|
||||||
profileppid: '/darmasaba/ppid/profile',
|
profileppid: () => '/darmasaba/ppid/profile',
|
||||||
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik',
|
daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik',
|
||||||
perbekeldarmasaba: '/darmasaba/desa/profile',
|
perbekeldarmasaba: () => '/darmasaba/desa/profile',
|
||||||
berita: `/darmasaba/desa/berita/${kategori}/${id}`,
|
berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`,
|
||||||
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`,
|
pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`,
|
||||||
sejarahdesa: '/darmasaba/desa/profile',
|
sejarahdesa: () => '/darmasaba/desa/profile',
|
||||||
visimisidesa: '/darmasaba/desa/profile',
|
visimisidesa: () => '/darmasaba/desa/profile',
|
||||||
lambangdesa: '/darmasaba/desa/profile',
|
lambangdesa: () => '/darmasaba/desa/profile',
|
||||||
maskotdesa: '/darmasaba/desa/profile',
|
maskotdesa: () => '/darmasaba/desa/profile',
|
||||||
profilperbekel: '/darmasaba/desa/profile',
|
profilperbekel: () => '/darmasaba/desa/profile',
|
||||||
potensi: '/darmasaba/desa/potensi-desa',
|
potensi: () => '/darmasaba/desa/potensi-desa',
|
||||||
galleryFoto: '/darmasaba/desa/gallery/foto',
|
galleryFoto: () => '/darmasaba/desa/gallery/foto',
|
||||||
galleryVideo: '/darmasaba/desa/gallery/video',
|
galleryVideo: () => '/darmasaba/desa/gallery/video',
|
||||||
pelayananSuratKeterangan: '/darmasaba/desa/layanan',
|
pelayananSuratKeterangan: () => '/darmasaba/desa/layanan',
|
||||||
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan',
|
pelayananPerizinanBerusaha: () => '/darmasaba/desa/layanan',
|
||||||
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan',
|
pelayananTelunjukSaktiDesa: () => '/darmasaba/desa/layanan',
|
||||||
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan',
|
pelayananPendudukNonPermanent: () => '/darmasaba/desa/layanan',
|
||||||
penghargaan: '/darmasaba/desa/penghargaan',
|
penghargaan: () => '/darmasaba/desa/penghargaan',
|
||||||
posyandu: '/darmasaba/kesehatan/posyandu',
|
posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`,
|
||||||
fasilitasKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||||
jadwalKegiatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||||
artikelKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||||
puskesmas: '/darmasaba/kesehatan/puskesmas',
|
puskesmas: () => '/darmasaba/kesehatan/puskesmas',
|
||||||
programKesehatan: '/darmasaba/kesehatan/program-kesehatan',
|
programKesehatan: () => '/darmasaba/kesehatan/program-kesehatan',
|
||||||
penangananDarurat: '/darmasaba/kesehatan/penanganan-darurat',
|
penangananDarurat: () => '/darmasaba/kesehatan/penanganan-darurat',
|
||||||
kontakDarurat: '/darmasaba/kesehatan/kontak-darurat',
|
kontakDarurat: () => '/darmasaba/kesehatan/kontak-darurat',
|
||||||
infoWabahPenyakit: '/darmasaba/kesehatan/info-wabah-penyakit',
|
infoWabahPenyakit: () => '/darmasaba/kesehatan/info-wabah-penyakit',
|
||||||
keamananLingkungan: '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
|
keamananLingkungan: () => '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
|
||||||
polsekTerdekat: '/darmasaba/keamanan/polsek-terdekat',
|
polsekTerdekat: () => '/darmasaba/keamanan/polsek-terdekat',
|
||||||
kontakDaruratKeamanan: '/darmasaba/keamanan/kontak-darurat',
|
kontakDaruratKeamanan: () => '/darmasaba/keamanan/kontak-darurat',
|
||||||
pencegahanKriminalitas: '/darmasaba/keamanan/pencegahan-kriminalitas',
|
pencegahanKriminalitas: () => '/darmasaba/keamanan/pencegahan-kriminalitas',
|
||||||
laporanPublik: '/darmasaba/keamanan/laporan-publik',
|
laporanPublik: () => '/darmasaba/keamanan/laporan-publik',
|
||||||
tipsKeamanan: '/darmasaba/keamanan/tips-keamanan',
|
tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan',
|
||||||
pasarDesa: '/darmasaba/ekonomi/pasar-desa',
|
pasarDesa: () => '/darmasaba/ekonomi/pasar-desa',
|
||||||
lowonganKerjaLokal: '/darmasaba/ekonomi/lowongan-kerja-lokal',
|
lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal',
|
||||||
strukturOrganisasi: '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
|
strukturOrganisasi: () => '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
|
||||||
jumlahPendudukUsiaKerjaYangMenganggurUsia: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||||
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||||
jumlahPendudukMiskin: '/darmasaba/ekonomi/jumlah-penduduk-miskin',
|
jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin',
|
||||||
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
|
programKemiskinan: () => '/darmasaba/ekonomi/program-kemiskinan',
|
||||||
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
|
sektorUnggulanDesa: () => '/darmasaba/ekonomi/sektor-unggulan-desa',
|
||||||
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
|
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',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return typeUrlMap[type || ''] || '/darmasaba';
|
if (type && map[type]) return map[type](id, kategori as string | undefined);
|
||||||
|
return '/darmasaba';
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getDetailUrl;
|
export default getDetailUrl;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user