Compare commits
5 Commits
nico/13-ok
...
nico/17-ok
| Author | SHA1 | Date | |
|---|---|---|---|
| bbf13c1cf7 | |||
| 75bf0652b1 | |||
| 0b574406e2 | |||
| ccf39bc778 | |||
| 3c21f7742c |
@@ -82,6 +82,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",
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ model MediaSosial {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= PROFILE ========================================= //
|
//========================================= DESA ANTI KORUPSI ========================================= //
|
||||||
model DesaAntiKorupsi {
|
model DesaAntiKorupsi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
@@ -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,52 @@ 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.DataPerpustakaanGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
kategori: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "", kategori = "") => {
|
||||||
|
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
dataPerpustakaan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = {};
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||||
|
"findManyAll"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||||
|
} else {
|
||||||
|
dataPerpustakaan.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||||
|
dataPerpustakaan.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
dataPerpustakaan.findManyAll.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -330,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 ?? [];
|
||||||
@@ -522,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 = {
|
||||||
@@ -533,7 +585,7 @@ const defaultPeminjamanBuku = {
|
|||||||
tanggalPinjam: "",
|
tanggalPinjam: "",
|
||||||
batasKembali: "",
|
batasKembali: "",
|
||||||
tanggalKembali: "",
|
tanggalKembali: "",
|
||||||
catatan: ""
|
catatan: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FormEditData {
|
interface FormEditData {
|
||||||
@@ -549,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 = {
|
||||||
@@ -561,8 +613,8 @@ const editForm: FormEditData = {
|
|||||||
batasKembali: "",
|
batasKembali: "",
|
||||||
tanggalKembali: "",
|
tanggalKembali: "",
|
||||||
catatan: "",
|
catatan: "",
|
||||||
status: "Dipinjam"
|
status: "Dipinjam",
|
||||||
}
|
};
|
||||||
|
|
||||||
const peminjamanBuku = proxy({
|
const peminjamanBuku = proxy({
|
||||||
create: {
|
create: {
|
||||||
@@ -616,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 ?? [];
|
||||||
@@ -685,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);
|
||||||
@@ -733,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 {
|
||||||
@@ -776,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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -795,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);
|
||||||
@@ -814,7 +873,7 @@ const peminjamanBuku = proxy({
|
|||||||
peminjamanBuku.update.form = { ...editForm };
|
peminjamanBuku.update.form = { ...editForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const perpustakaanDigitalState = proxy({
|
const perpustakaanDigitalState = proxy({
|
||||||
dataPerpustakaan,
|
dataPerpustakaan,
|
||||||
|
|||||||
@@ -561,6 +561,45 @@ const pegawai = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findManyAll: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.PegawaiPPIDGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
posisi: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pegawai.findManyAll.loading = true; // Use the full path to access the property
|
||||||
|
pegawai.findManyAll.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { search };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||||
|
"find-many-all"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pegawai.findManyAll.data = res.data.data || [];
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load pegawai:", res.data?.message);
|
||||||
|
pegawai.findManyAll.data = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
|
pegawai.findManyAll.data = [];
|
||||||
|
} finally {
|
||||||
|
pegawai.findManyAll.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
| (Prisma.PegawaiPPIDGetPayload<{
|
| (Prisma.PegawaiPPIDGetPayload<{
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function PelayananPendudukNonPermanent() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pelayananPendudukNonPermanen.findById.load('1');
|
pelayananPendudukNonPermanen.findById.load('edit');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!pelayananPendudukNonPermanen.findById.data) {
|
if (!pelayananPendudukNonPermanen.findById.data) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function PerizinanBerusaha() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// You should get the ID from your router query or params
|
// You should get the ID from your router query or params
|
||||||
const id = '1'; // Replace with actual ID or get from URL params
|
const id = 'edit'; // Replace with actual ID or get from URL params
|
||||||
await pelayananPerizinanBerusaha.findById.load(id);
|
await pelayananPerizinanBerusaha.findById.load(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Gagal memuat data');
|
setError('Gagal memuat data');
|
||||||
|
|||||||
@@ -89,26 +89,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
|
|||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Judul</TableTh>
|
<TableTh style={{ minWidth: 200 }}>Judul</TableTh>
|
||||||
<TableTh>Konten</TableTh>
|
<TableTh style={{ minWidth: 200 }}>Konten</TableTh>
|
||||||
<TableTh>Aksi</TableTh>
|
<TableTh style={{ minWidth: 200 }}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd style={{ minWidth: 200 }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd style={{ minWidth: 200 }} >
|
||||||
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
|
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
|
||||||
{item.content}
|
{item.content}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd style={{ minWidth: 200 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
|
|
||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
||||||
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
|
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
|
||||||
{mounted && diseaseChartData.length > 0 ? (
|
{mounted && diseaseChartData.length > 0 ? (
|
||||||
<Center>
|
<Center>
|
||||||
|
|||||||
@@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
|
|||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
<Box w={200}>
|
||||||
<Text truncate fz="sm" c="dimmed">
|
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
|
||||||
{item.deskripsiSingkat}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
Profil Desa
|
Profile Desa
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { DateInput } from '@mantine/dates';
|
import { DateInput } from '@mantine/dates';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -56,6 +57,10 @@ function EditPeminjam() {
|
|||||||
catatan: '',
|
catatan: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPeminjam = async () => {
|
const loadPeminjam = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -159,13 +164,17 @@ function EditPeminjam() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<Box>
|
||||||
value={formData.buku?.judul || ''}
|
<Text fw="bold" fz="sm" mb={6}>Buku</Text>
|
||||||
label={<Text fw="bold" fz="sm">Buku</Text>}
|
<Select
|
||||||
placeholder="Buku"
|
placeholder="Pilih buku"
|
||||||
required
|
data={perpustakaanDigitalState.dataPerpustakaan.findManyAll.data?.map(p => ({ value: p.id, label: p.judul })) || []}
|
||||||
readOnly
|
value={formData.bukuId}
|
||||||
|
onChange={(value) => value && setFormData({ ...formData, bukuId: value })}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
value={formData.tanggalPinjam}
|
value={formData.tanggalPinjam}
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
|
|||||||
@@ -103,18 +103,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
|||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{(() => {
|
{filteredData.map((item) => (
|
||||||
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
|
|
||||||
return null;
|
|
||||||
})()}
|
|
||||||
{([...filteredData]
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.isActive === b.isActive) {
|
|
||||||
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
|
|
||||||
}
|
|
||||||
return Number(b.isActive) - Number(a.isActive); // aktif duluan
|
|
||||||
}) // Aktif di atas
|
|
||||||
).map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Box w={150}>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ function ListStrukturOrganisasiPPID() {
|
|||||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateOrganisasi.findMany.load();
|
stateOrganisasi.findManyAll.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (stateOrganisasi.findMany.loading) {
|
if (stateOrganisasi.findManyAll.loading) {
|
||||||
return (
|
return (
|
||||||
<Center py={40}>
|
<Center py={40}>
|
||||||
<Loader size="lg" />
|
<Loader size="lg" />
|
||||||
@@ -32,7 +32,7 @@ function ListStrukturOrganisasiPPID() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
if (!stateOrganisasi.findManyAll.data || stateOrganisasi.findManyAll.data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Stack align="center" py={60} gap="sm">
|
<Stack align="center" py={60} gap="sm">
|
||||||
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
|
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
|
||||||
@@ -43,7 +43,7 @@ function ListStrukturOrganisasiPPID() {
|
|||||||
|
|
||||||
const posisiMap = new Map<string, any>();
|
const posisiMap = new Map<string, any>();
|
||||||
|
|
||||||
const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
|
const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
|
||||||
|
|
||||||
for (const pegawai of aktifPegawai) {
|
for (const pegawai of aktifPegawai) {
|
||||||
const posisiId = pegawai.posisi.id;
|
const posisiId = pegawai.posisi.id;
|
||||||
|
|||||||
@@ -377,22 +377,5 @@ export const navBar = [
|
|||||||
path: "/admin/pendidikan/data-pendidikan"
|
path: "/admin/pendidikan/data-pendidikan"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: "User & Role",
|
|
||||||
name: "User & Role",
|
|
||||||
path: "",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: "User",
|
|
||||||
name: "User",
|
|
||||||
path: "/admin/user&role/user"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Role",
|
|
||||||
name: "Role",
|
|
||||||
path: "/admin/user&role/role"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function dataPerpustakaanFindManyAll(context: Context) {
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
|
const isActiveParam = context.query.isActive;
|
||||||
|
|
||||||
|
// Buat where clause dinamis
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
if (isActiveParam !== undefined) {
|
||||||
|
where.isActive = isActiveParam === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ judul: { contains: search, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await prisma.dataPerpustakaan.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
kategori: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success fetch all data perpustakaan (non-paginated)",
|
||||||
|
total: data.length,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find many all error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed fetch all data perpustakaan",
|
||||||
|
total: 0,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import dataPerpustakaanDelete from "./del";
|
|||||||
import dataPerpustakaanFindMany from "./findMany";
|
import dataPerpustakaanFindMany from "./findMany";
|
||||||
import dataPerpustakaanFindUnique from "./findUnique";
|
import dataPerpustakaanFindUnique from "./findUnique";
|
||||||
import dataPerpustakaanUpdate from "./updt";
|
import dataPerpustakaanUpdate from "./updt";
|
||||||
|
import dataPerpustakaanFindManyAll from "./findManyAll";
|
||||||
|
|
||||||
const DataPerpustakaan = new Elysia({
|
const DataPerpustakaan = new Elysia({
|
||||||
prefix: "/dataperpustakaan",
|
prefix: "/dataperpustakaan",
|
||||||
@@ -18,7 +19,7 @@ const DataPerpustakaan = new Elysia({
|
|||||||
kategoriId: t.String(),
|
kategoriId: t.String(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
.get("/findManyAll", dataPerpustakaanFindManyAll)
|
||||||
.get("/findMany", dataPerpustakaanFindMany)
|
.get("/findMany", dataPerpustakaanFindMany)
|
||||||
.get("/:id", async (context) => {
|
.get("/:id", async (context) => {
|
||||||
const response = await dataPerpustakaanFindUnique(
|
const response = await dataPerpustakaanFindUnique(
|
||||||
|
|||||||
@@ -2,61 +2,67 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
// Di findMany.ts
|
|
||||||
export default async function pegawaiFindMany(context: Context) {
|
export default async function pegawaiFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
const search = (context.query.search as string) || "";
|
const search = (context.query.search as string) || "";
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
// Buat where clause
|
|
||||||
const isActiveParam = context.query.isActive;
|
const isActiveParam = context.query.isActive;
|
||||||
|
|
||||||
|
// where clause dinamis
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
if (isActiveParam !== undefined) {
|
if (isActiveParam !== undefined) {
|
||||||
where.isActive = isActiveParam === "true";
|
where.isActive = isActiveParam === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tambahkan pencarian (jika ada)
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
||||||
{ alamat: { contains: search, mode: "insensitive" } },
|
{ alamat: { contains: search, mode: "insensitive" } },
|
||||||
|
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
// Ambil semua data terlebih dahulu (tanpa pagination)
|
||||||
|
const [allData, total] = await Promise.all([
|
||||||
prisma.pegawaiPPID.findMany({
|
prisma.pegawaiPPID.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
posisi: true,
|
posisi: true,
|
||||||
image: true,
|
image: true,
|
||||||
},
|
},
|
||||||
skip,
|
|
||||||
take: limit,
|
|
||||||
orderBy: { posisi: { hierarki: "asc" } },
|
|
||||||
}),
|
|
||||||
prisma.pegawaiPPID.count({
|
|
||||||
where,
|
|
||||||
}),
|
}),
|
||||||
|
prisma.pegawaiPPID.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Sort manual berdasarkan hierarki posisi
|
||||||
|
const sortedData = allData.sort((a, b) => {
|
||||||
|
// Sort berdasarkan hierarki terlebih dahulu
|
||||||
|
if (a.posisi.hierarki !== b.posisi.hierarki) {
|
||||||
|
return a.posisi.hierarki - b.posisi.hierarki;
|
||||||
|
}
|
||||||
|
// Jika hierarki sama, sort berdasarkan nama posisi
|
||||||
|
return a.posisi.nama.localeCompare(b.posisi.nama);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lakukan pagination manual setelah sorting
|
||||||
|
const paginatedData = sortedData.slice(skip, skip + limit);
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success fetch pegawai with pagination",
|
message: "Success fetch pegawai with hierarchy order",
|
||||||
data,
|
data: paginatedData,
|
||||||
page,
|
page,
|
||||||
totalPages,
|
totalPages,
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error("Find many paginated error:", e);
|
console.error("Find many pegawai error:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Failed fetch pegawai with pagination",
|
message: "Failed fetch pegawai",
|
||||||
data: [],
|
data: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
export default async function pegawaiFindManyAll(context: Context) {
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
|
const isActiveParam = context.query.isActive;
|
||||||
|
|
||||||
|
// Buat where clause dinamis
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
if (isActiveParam !== undefined) {
|
||||||
|
where.isActive = isActiveParam === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
||||||
|
{ alamat: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await prisma.pegawaiPPID.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
posisi: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
|
orderBy: { posisi: { hierarki: "asc" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success fetch all pegawai (non-paginated)",
|
||||||
|
total: data.length,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find many all error:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed fetch all pegawai",
|
||||||
|
total: 0,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import pegawaiCreate from "./create";
|
|||||||
import pegawaiNonActive from "./nonActive";
|
import pegawaiNonActive from "./nonActive";
|
||||||
import pegawaiUpdate from "./updt";
|
import pegawaiUpdate from "./updt";
|
||||||
import pegawaiDelete from "./del";
|
import pegawaiDelete from "./del";
|
||||||
|
import pegawaiFindManyAll from "./findManyAll";
|
||||||
|
|
||||||
|
|
||||||
const Pegawai = new Elysia({
|
const Pegawai = new Elysia({
|
||||||
@@ -15,6 +16,9 @@ const Pegawai = new Elysia({
|
|||||||
// ✅ Find all
|
// ✅ Find all
|
||||||
.get("/find-many", pegawaiFindMany)
|
.get("/find-many", pegawaiFindMany)
|
||||||
|
|
||||||
|
// ✅ Find all (non-paginated)
|
||||||
|
.get("/find-many-all", pegawaiFindManyAll)
|
||||||
|
|
||||||
// ✅ Find by ID
|
// ✅ Find by ID
|
||||||
.get("/:id", async (context) => {
|
.get("/:id", async (context) => {
|
||||||
const response = await pegawaiFindUnique(context);
|
const response = await pegawaiFindUnique(context);
|
||||||
|
|||||||
1970
src/app/api/[[...slugs]]/_lib/search/findMany.ts
Normal file
1970
src/app/api/[[...slugs]]/_lib/search/findMany.ts
Normal file
File diff suppressed because it is too large
Load Diff
10
src/app/api/[[...slugs]]/_lib/search/index.ts
Normal file
10
src/app/api/[[...slugs]]/_lib/search/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
import searchFindMany from "./findMany";
|
||||||
|
|
||||||
|
const Search = new Elysia({
|
||||||
|
prefix: "/api/search",
|
||||||
|
tags: ["Search"],
|
||||||
|
})
|
||||||
|
.get("/findMany", searchFindMany);
|
||||||
|
|
||||||
|
export default Search;
|
||||||
148
src/app/api/[[...slugs]]/_lib/search/searchState.ts
Normal file
148
src/app/api/[[...slugs]]/_lib/search/searchState.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
import { proxy } from 'valtio';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
type?: string;
|
||||||
|
id: string | number;
|
||||||
|
title?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchState = proxy({
|
||||||
|
query: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
type: '', // kosong = global search
|
||||||
|
results: [] as SearchResult[],
|
||||||
|
nextPage: null as number | null,
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
if (!searchState.query) {
|
||||||
|
searchState.results = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchState.loading = true;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.search.findMany.get({
|
||||||
|
query: {
|
||||||
|
query: searchState.query,
|
||||||
|
page: searchState.page,
|
||||||
|
limit: searchState.limit,
|
||||||
|
type: searchState.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchState.page === 1) {
|
||||||
|
searchState.results = res.data?.data || [];
|
||||||
|
} else {
|
||||||
|
searchState.results.push(...(res.data?.data || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
searchState.nextPage = res.data?.nextPage || null;
|
||||||
|
searchState.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async next() {
|
||||||
|
if (!searchState.nextPage || searchState.loading) return;
|
||||||
|
searchState.page = searchState.nextPage;
|
||||||
|
await searchState.fetch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🕒 debounce-nya tetap kita export biar bisa dipanggil manual
|
||||||
|
export const debouncedFetch = debounce(() => {
|
||||||
|
searchState.page = 1;
|
||||||
|
searchState.fetch();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
export default searchState;
|
||||||
|
|
||||||
|
|
||||||
|
// 'use client';
|
||||||
|
// import { proxy, subscribe } from 'valtio';
|
||||||
|
// import { debounce } from 'lodash';
|
||||||
|
// import ApiFetch from '@/lib/api-fetch';
|
||||||
|
|
||||||
|
// interface SearchResult {
|
||||||
|
// type?: string;
|
||||||
|
// id: string | number;
|
||||||
|
// title?: string;
|
||||||
|
// [key: string]: any;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const searchState = proxy({
|
||||||
|
// query: '',
|
||||||
|
// page: 1,
|
||||||
|
// limit: 10,
|
||||||
|
// type: '', // kosong = global search
|
||||||
|
// results: [] as SearchResult[],
|
||||||
|
// nextPage: null as number | null,
|
||||||
|
// loading: false,
|
||||||
|
|
||||||
|
// // --- fetch utama ---
|
||||||
|
// async fetch() {
|
||||||
|
// if (!searchState.query.trim()) {
|
||||||
|
// // 🧹 kalau query kosong, kosongin data dan stop
|
||||||
|
// searchState.results = [];
|
||||||
|
// searchState.nextPage = null;
|
||||||
|
// searchState.loading = false;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// searchState.loading = true;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const res = await ApiFetch.api.search.findMany.get({
|
||||||
|
// query: {
|
||||||
|
// query: searchState.query,
|
||||||
|
// page: searchState.page,
|
||||||
|
// limit: searchState.limit,
|
||||||
|
// type: searchState.type,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const newData = res.data?.data || [];
|
||||||
|
|
||||||
|
// // Kalau ini page pertama, replace data
|
||||||
|
// if (searchState.page === 1) {
|
||||||
|
// searchState.results = newData;
|
||||||
|
// } else {
|
||||||
|
// // Kalau page berikutnya, append data
|
||||||
|
// searchState.results = [...searchState.results, ...newData];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// searchState.nextPage = res.data?.nextPage || null;
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Search fetch error:', err);
|
||||||
|
// } finally {
|
||||||
|
// searchState.loading = false;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
|
||||||
|
// // --- load next page (infinite scroll) ---
|
||||||
|
// async next() {
|
||||||
|
// if (!searchState.nextPage || searchState.loading) return;
|
||||||
|
// searchState.page = searchState.nextPage;
|
||||||
|
// await searchState.fetch();
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // --- debounce agar gak fetch tiap ketik ---
|
||||||
|
// const debouncedFetch = debounce(() => {
|
||||||
|
// // reset pagination setiap query berubah
|
||||||
|
// searchState.page = 1;
|
||||||
|
// searchState.fetch();
|
||||||
|
// }, 500);
|
||||||
|
|
||||||
|
// // --- auto trigger setiap query berubah ---
|
||||||
|
// subscribe(searchState, () => {
|
||||||
|
// // kalau query berubah, jalankan debounce fetch
|
||||||
|
// debouncedFetch();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export default searchState;
|
||||||
@@ -25,6 +25,7 @@ import LandingPage from "./_lib/landing_page";
|
|||||||
import Pendidikan from "./_lib/pendidikan";
|
import Pendidikan from "./_lib/pendidikan";
|
||||||
import User from "./_lib/user";
|
import User from "./_lib/user";
|
||||||
import Role from "./_lib/user/role";
|
import Role from "./_lib/user/role";
|
||||||
|
import Search from "./_lib/search";
|
||||||
|
|
||||||
const ROOT = process.cwd();
|
const ROOT = process.cwd();
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ const ApiServer = new Elysia()
|
|||||||
.use(Pendidikan)
|
.use(Pendidikan)
|
||||||
.use(User)
|
.use(User)
|
||||||
.use(Role)
|
.use(Role)
|
||||||
|
.use(Search)
|
||||||
|
|
||||||
.onError(({ code }) => {
|
.onError(({ code }) => {
|
||||||
if (code === "NOT_FOUND") {
|
if (code === "NOT_FOUND") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function PelayananPendudukNonPermanent() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await state.pelayananPendudukNonPermanen.findById.load('1');
|
await state.pelayananPendudukNonPermanen.findById.load('edit');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gagal memuat data:', error);
|
console.error('Gagal memuat data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function PelayananPerizinanBerusaha() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await state.pelayananPerizinanBerusaha.findById.load('1')
|
await state.pelayananPerizinanBerusaha.findById.load('edit')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gagal memuat data:', error);
|
console.error('Gagal memuat data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ function Page() {
|
|||||||
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
|
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8}>
|
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
|
||||||
{state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.'}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import ProfilPerbekel from './ui/profilPerbekel';
|
|||||||
// import LembagaDesa from './ui/lembagaDesa';
|
// import LembagaDesa from './ui/lembagaDesa';
|
||||||
import MotoDesa from './ui/motoDesa';
|
import MotoDesa from './ui/motoDesa';
|
||||||
import SemuaPerbekel from './ui/semuaPerbekel';
|
import SemuaPerbekel from './ui/semuaPerbekel';
|
||||||
|
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
return (
|
return (
|
||||||
|
<Box>
|
||||||
<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 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
@@ -35,6 +37,9 @@ function Page() {
|
|||||||
<SemuaPerbekel />
|
<SemuaPerbekel />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{/* Tombol Scroll ke Atas */}
|
||||||
|
<ScrollToTopButton />
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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, Title } 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 { 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';
|
||||||
@@ -56,9 +56,12 @@ function Page() {
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex 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: "50%", md: "100%" }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -41,6 +41,37 @@ function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||||
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
|
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||||
|
Pencegahan Kriminalitas
|
||||||
|
</Text>
|
||||||
|
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
|
||||||
|
Keamanan Komunitas & Pencegahan Kriminal
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<SimpleGrid
|
||||||
|
px={{ base: 20, md: 100 }}
|
||||||
|
cols={{ base: 1, md: 2 }}
|
||||||
|
spacing="xl"
|
||||||
|
>
|
||||||
|
<Paper p="xl" radius="xl" shadow="lg" >
|
||||||
|
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
|
||||||
|
Program Keamanan Berjalan
|
||||||
|
</Text>
|
||||||
|
<Stack pt={30} gap="lg">
|
||||||
|
<Text c="dimmed">
|
||||||
|
Tidak ada data pencegahan kriminalitas yang cocok
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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 }}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { BarChart as MantineBarChart } from '@mantine/charts';
|
import { BarChart as MantineBarChart } from '@mantine/charts';
|
||||||
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
|
|
||||||
@@ -107,20 +107,7 @@ function Page() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||||
<Box pb={30}>
|
<Box pb={30}>
|
||||||
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
|
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
|
||||||
<Box>
|
|
||||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
|
||||||
<ColorSwatch color="#EF3E3E" size={30} />
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
|
||||||
<ColorSwatch color="#3290CA" size={30} />
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
{chartData.length === 0 ? (
|
{chartData.length === 0 ? (
|
||||||
<Text c="dimmed" ta="center" py="xl">
|
<Text c="dimmed" ta="center" py="xl">
|
||||||
Belum ada data yang tersedia untuk ditampilkan
|
Belum ada data yang tersedia untuk ditampilkan
|
||||||
@@ -150,6 +137,20 @@ function Page() {
|
|||||||
</Center>
|
</Center>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||||
|
<Box>
|
||||||
|
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||||
|
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||||
|
<ColorSwatch color="#EF3E3E" size={30} />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||||
|
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
||||||
|
<ColorSwatch color="#3290CA" size={30} />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -121,13 +121,13 @@ function Page() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dimmed">
|
||||||
Diposting: 12 Februari 2025 · Dinas Kesehatan
|
Diposting: {v.createdAt.toLocaleDateString()}
|
||||||
</Text>
|
</Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text fz="sm" lh={1.5}>
|
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
|
||||||
{v.deskripsiSingkat}
|
{v.deskripsiSingkat}
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}>
|
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
|
||||||
Selengkapnya
|
Selengkapnya
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
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,
|
||||||
Center,
|
Center,
|
||||||
Grid,
|
Grid,
|
||||||
@@ -97,19 +96,39 @@ function Page() {
|
|||||||
shadow="sm"
|
shadow="sm"
|
||||||
withBorder
|
withBorder
|
||||||
bg={colors['white-trans-1']}
|
bg={colors['white-trans-1']}
|
||||||
style={{ transition: 'all 0.3s ease' }}
|
style={{
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-5px)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||||
>
|
>
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Center>
|
<Center>
|
||||||
|
<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={160}
|
fit="cover"
|
||||||
h={160}
|
|
||||||
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>
|
||||||
|
|
||||||
</Center>
|
</Center>
|
||||||
<Stack gap={4} w="100%">
|
<Stack gap={4} w="100%">
|
||||||
<Text
|
<Text
|
||||||
@@ -131,9 +150,6 @@ function Page() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Badge radius="md" color="blue" variant="light" mt="sm">
|
|
||||||
Darurat
|
|
||||||
</Badge>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
@@ -151,8 +167,11 @@ function Page() {
|
|||||||
styles={{
|
styles={{
|
||||||
control: {
|
control: {
|
||||||
border: `1px solid ${colors['blue-button']}`,
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/ico
|
|||||||
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 { useDebouncedValue } from "@mantine/hooks";
|
||||||
|
|
||||||
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 { 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 (
|
||||||
@@ -28,11 +30,31 @@ export default function Page() {
|
|||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box py="xl" px={{ base: "md", md: 100 }}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
|
||||||
<Text fz="lg" fw="bold" c={colors["blue-button"]}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
Tidak ada posyandu yang ditemukan
|
<BackButton />
|
||||||
|
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
|
||||||
|
<Text
|
||||||
|
ta="left"
|
||||||
|
fz={{ base: "1.8rem", md: "2.5rem" }}
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
fw="bold"
|
||||||
|
>
|
||||||
|
Posyandu Desa Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari posyandu berdasarkan nama..."
|
||||||
|
aria-label="Pencarian Posyandu"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
leftSection={<IconSearch size={20} />}
|
||||||
|
w={{ base: "100%", md: "35%" }}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,7 @@ function Page() {
|
|||||||
<Tooltip label={materi.data?.judul} position="top" withArrow>
|
<Tooltip label={materi.data?.judul} position="top" withArrow>
|
||||||
<Stack gap={4} align="center">
|
<Stack gap={4} align="center">
|
||||||
<IconRecycle size={28} color={colors['blue-button']} />
|
<IconRecycle size={28} color={colors['blue-button']} />
|
||||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center" dangerouslySetInnerHTML={{ __html: materi.data?.judul || '' }} />
|
||||||
{materi.data?.judul}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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,47 +1,64 @@
|
|||||||
|
/* 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)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!featured.data && !loadingFeatured) {
|
if (!featured.data && !loadingFeatured) {
|
||||||
gotongRoyongState.kegiatanDesa.findFirst.load();
|
gotongRoyongState.kegiatanDesa.findFirst.load();
|
||||||
}
|
}
|
||||||
}, [featured.data, loadingFeatured]);
|
}, [featured.data, loadingFeatured]);
|
||||||
|
|
||||||
// Load berita terbaru (untuk grid) saat page/search berubah
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const limit = 3; // Sesuaikan dengan tampilan grid
|
const limit = 3;
|
||||||
state.findMany.load(page, limit, search);
|
state.findMany.load(page, limit, search);
|
||||||
}, [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()}`);
|
router.replace(`?${url.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,14 +66,34 @@ function Page() {
|
|||||||
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 +115,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 +129,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 +150,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 +194,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 +249,6 @@ function Page() {
|
|||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</MotionBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDownload, IconSend2 } from '@tabler/icons-react';
|
import { IconSend2 } 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 BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
@@ -150,23 +149,6 @@ function Page() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Center pb={30}>
|
|
||||||
<Tooltip label="Unduh dokumen tata cara permohonan" withArrow>
|
|
||||||
<Button
|
|
||||||
fz="sm"
|
|
||||||
size="md"
|
|
||||||
radius="md"
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
leftSection={
|
|
||||||
<IconDownload size={20} color={colors['white-1']} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Unduh Tata Cara
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Center>
|
|
||||||
|
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Paper
|
<Paper
|
||||||
p="xl"
|
p="xl"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
|
|||||||
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
|
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } 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';
|
||||||
|
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const allList = useProxy(stateProfilePPID)
|
const allList = useProxy(stateProfilePPID)
|
||||||
@@ -36,6 +37,7 @@ function Page() {
|
|||||||
: [allList.profile.data]
|
: [allList.profile.data]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box>
|
||||||
<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 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
@@ -129,6 +131,9 @@ function Page() {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{/* Tombol Scroll ke Atas */}
|
||||||
|
<ScrollToTopButton />
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
157
src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx
Normal file
157
src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
'use client';
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function DetailPegawaiUser() {
|
||||||
|
const statePegawai = useProxy(stateStrukturPPID.pegawai);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||||
|
statePegawai.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
if (!statePegawai.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py="lg">
|
||||||
|
<Skeleton height={500} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = statePegawai.findUnique.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="xl">
|
||||||
|
{/* Back button */}
|
||||||
|
<Group mb="lg">
|
||||||
|
<Box
|
||||||
|
onClick={() => router.back()}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowBack size={22} color={colors['blue-button']} />
|
||||||
|
<Text c={colors['blue-button']} fw={500}>
|
||||||
|
Kembali
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '70%' }}
|
||||||
|
mx="auto"
|
||||||
|
p="xl"
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
bg="white"
|
||||||
|
style={{
|
||||||
|
border: '1px solid #eaeaea',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
{/* Foto Profil */}
|
||||||
|
<Image
|
||||||
|
src={data.image?.link || '/placeholder-profile.png'}
|
||||||
|
alt={data.namaLengkap || 'Foto Profil'}
|
||||||
|
w={160}
|
||||||
|
h={160}
|
||||||
|
radius={100}
|
||||||
|
fit="cover"
|
||||||
|
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Nama & Jabatan */}
|
||||||
|
<Stack align="center" gap={2}>
|
||||||
|
<Title order={3} fw={700} c={colors['blue-button']}>
|
||||||
|
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||||
|
</Title>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider my="lg" />
|
||||||
|
|
||||||
|
{/* Informasi Detail */}
|
||||||
|
<Stack gap="md">
|
||||||
|
<InfoRow label="Email" value={data.email} />
|
||||||
|
<InfoRow label="Telepon" value={data.telepon} />
|
||||||
|
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||||
|
<InfoRow
|
||||||
|
label="Tanggal Masuk"
|
||||||
|
value={
|
||||||
|
data.tanggalMasuk
|
||||||
|
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
label="Status"
|
||||||
|
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||||
|
valueColor={data.isActive ? 'green' : 'red'}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Komponen kecil untuk menampilkan baris informasi */
|
||||||
|
function InfoRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
valueColor,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: string | null;
|
||||||
|
valueColor?: string;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} c="dark">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c={valueColor || 'dimmed'}
|
||||||
|
style={{
|
||||||
|
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailPegawaiUser;
|
||||||
@@ -1,129 +1,9 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
// /* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
// 'use client'
|
|
||||||
// import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
|
||||||
// import colors from '@/con/colors';
|
|
||||||
// import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
|
||||||
// import { OrganizationChart } from 'primereact/organizationchart';
|
|
||||||
// import { useEffect } from 'react';
|
|
||||||
// import { useProxy } from 'valtio/utils';
|
|
||||||
// import BackButton from '../../desa/layanan/_com/BackButto';
|
|
||||||
|
|
||||||
// function Page() {
|
|
||||||
// return (
|
|
||||||
// <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
|
||||||
// <Box px={{ base: 'md', md: 100 }}>
|
|
||||||
// <BackButton />
|
|
||||||
// </Box>
|
|
||||||
// <Title ta={"center"} fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>Struktur PPID</Title>
|
|
||||||
// <StrukturOrganisasiPPID />
|
|
||||||
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function StrukturOrganisasiPPID() {
|
|
||||||
// const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// stateOrganisasi.findMany.load()
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
|
||||||
// return (
|
|
||||||
// <Stack py={10}>
|
|
||||||
// <Skeleton h={500} />
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Step 1: Group pegawai berdasarkan posisiId
|
|
||||||
// const posisiMap = new Map<string, any>();
|
|
||||||
|
|
||||||
// for (const pegawai of stateOrganisasi.findMany.data) {
|
|
||||||
// const posisiId = pegawai.posisi.id;
|
|
||||||
// if (!posisiMap.has(posisiId)) {
|
|
||||||
// posisiMap.set(posisiId, {
|
|
||||||
// ...pegawai.posisi,
|
|
||||||
// pegawaiList: [],
|
|
||||||
// children: []
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// // Step 2: Buat struktur pohon berdasarkan parentId
|
|
||||||
// const root: any[] = [];
|
|
||||||
|
|
||||||
// posisiMap.forEach((posisi) => {
|
|
||||||
// if (posisi.parentId) {
|
|
||||||
// const parent = posisiMap.get(posisi.parentId);
|
|
||||||
// if (parent) {
|
|
||||||
// parent.children.push(posisi);
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// root.push(posisi);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Step 3: Ubah struktur ke format OrganizationChart
|
|
||||||
// function toOrgChartFormat(node: any): any {
|
|
||||||
// return {
|
|
||||||
// expanded: true,
|
|
||||||
// type: 'person',
|
|
||||||
// styleClass: 'p-person',
|
|
||||||
// data: {
|
|
||||||
// name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
|
|
||||||
// status: node.nama,
|
|
||||||
// image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
|
|
||||||
// },
|
|
||||||
// children: node.children.map(toOrgChartFormat)
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// const chartData = root.map(toOrgChartFormat);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Box py={10}>
|
|
||||||
// <Paper bg={colors.grey} p="md" style={{ overflowX: 'auto' }}>
|
|
||||||
// <OrganizationChart style={{ color: colors['blue-button'] }} value={chartData} nodeTemplate={nodeTemplate} />
|
|
||||||
// </Paper>
|
|
||||||
// </Box>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// function nodeTemplate(node: any) {
|
|
||||||
// const imageSrc = node?.data?.image || '/img/default.png';
|
|
||||||
// const name = node?.data?.name || 'Tanpa Nama';
|
|
||||||
// const status = node?.data?.status || 'Tidak ada deskripsi';
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Stack pos={"relative"} py={"xl"} gap={"22"}>
|
|
||||||
// <Stack align="center" gap={4}>
|
|
||||||
// <Image
|
|
||||||
// src={imageSrc}
|
|
||||||
// alt={name}
|
|
||||||
// radius="xl"
|
|
||||||
// w={120}
|
|
||||||
// h={120}
|
|
||||||
// fit="cover"
|
|
||||||
// />
|
|
||||||
// <Text fw={600} ta="center">{name}</Text>
|
|
||||||
// <Text size="sm" c="dimmed" ta="center">{status}</Text>
|
|
||||||
// </Stack>
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default Page;
|
|
||||||
|
|
||||||
'use client'
|
'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 colors from '@/con/colors'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -136,16 +16,27 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Transition,
|
Transition,
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
|
import {
|
||||||
|
IconRefresh,
|
||||||
|
IconSearch,
|
||||||
|
IconUsers,
|
||||||
|
IconZoomIn,
|
||||||
|
IconZoomOut,
|
||||||
|
IconArrowsMaximize,
|
||||||
|
IconArrowsMinimize,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
import { OrganizationChart } from 'primereact/organizationchart'
|
import { 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 colors from '@/con/colors'
|
import { useTransitionRouter } from 'next-view-transitions'
|
||||||
|
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
@@ -167,7 +58,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>
|
||||||
@@ -180,20 +70,34 @@ export default function Page() {
|
|||||||
<StrukturOrganisasiPPID />
|
<StrukturOrganisasiPPID />
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
{/* Tombol Scroll ke Atas */}
|
||||||
|
<ScrollToTopButton />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StrukturOrganisasiPPID() {
|
function StrukturOrganisasiPPID() {
|
||||||
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
||||||
|
const router = useTransitionRouter()
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [scale, setScale] = useState(1)
|
||||||
|
const [isFullscreen, setFullscreen] = useState(false)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
|
// debounce untuk pencarian
|
||||||
|
const debouncedSearch = useRef(
|
||||||
|
debounce((value: string) => {
|
||||||
|
setSearchQuery(value)
|
||||||
|
}, 400)
|
||||||
|
).current
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
||||||
@@ -209,10 +113,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">
|
||||||
@@ -233,8 +134,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
|
||||||
@@ -245,15 +145,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>
|
||||||
@@ -261,45 +152,42 @@ 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]
|
||||||
return {
|
return {
|
||||||
expanded: true,
|
expanded: true,
|
||||||
type: 'person',
|
type: 'person',
|
||||||
styleClass: 'p-person',
|
styleClass: 'p-person',
|
||||||
data: {
|
data: {
|
||||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
|
id: pegawai?.id || null,
|
||||||
|
name: pegawai?.namaLengkap || 'Belum ditugaskan',
|
||||||
title: node.nama || 'Tanpa jabatan',
|
title: node.nama || 'Tanpa jabatan',
|
||||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
|
image: pegawai?.image?.link || '/img/default.png',
|
||||||
description: node.deskripsi || '',
|
description: node.deskripsi || '',
|
||||||
positionId: node.id || null,
|
positionId: node.id || null,
|
||||||
},
|
},
|
||||||
@@ -307,29 +195,91 @@ function StrukturOrganisasiPPID() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = root.map(toOrgChartFormat)
|
let chartData = root.map(toOrgChartFormat)
|
||||||
|
|
||||||
|
// 🔍 filter by search
|
||||||
|
if (searchQuery) {
|
||||||
|
const filterNodes = (nodes: any[]): any[] =>
|
||||||
|
nodes
|
||||||
|
.map((n) => ({
|
||||||
|
...n,
|
||||||
|
children: filterNodes(n.children || []),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
(n) =>
|
||||||
|
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
n.children.length > 0
|
||||||
|
)
|
||||||
|
chartData = filterNodes(chartData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧭 fungsi fullscreen
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
chartContainerRef.current?.requestFullscreen()
|
||||||
|
setFullscreen(true)
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen()
|
||||||
|
setFullscreen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧭 fungsi zoom
|
||||||
|
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
|
||||||
|
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
|
||||||
|
const resetZoom = () => setScale(1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={16} >
|
<Stack align="center" mt="xl">
|
||||||
<Paper
|
{/* 🔍 Search + Zoom + Fullscreen controls */}
|
||||||
radius="md"
|
<Group mb="md" justify="center" gap="sm">
|
||||||
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>
|
||||||
|
<Button variant="light" size="sm" onClick={resetZoom}>
|
||||||
|
100%
|
||||||
|
</Button>
|
||||||
|
<Button variant="light" size="sm" onClick={handleZoomIn}>
|
||||||
|
<IconZoomIn size={16} />
|
||||||
|
</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={{
|
style={{
|
||||||
background: 'rgba(28,110,164,0.2)',
|
overflow: 'auto',
|
||||||
border: `1px solid rgba(255,255,255,0.1)`,
|
transform: `scale(${scale})`,
|
||||||
overflowX: 'auto',
|
transformOrigin: 'center top',
|
||||||
|
transition: 'transform 0.25s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OrganizationChart
|
<OrganizationChart
|
||||||
value={chartData}
|
value={chartData}
|
||||||
nodeTemplate={nodeTemplate}
|
nodeTemplate={(node) => nodeTemplate(node, router)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeTemplate(node: any) {
|
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
|
||||||
const imageSrc = node?.data?.image || '/img/default.png'
|
const imageSrc = node?.data?.image || '/img/default.png'
|
||||||
const name = node?.data?.name || 'Tanpa Nama'
|
const name = node?.data?.name || 'Tanpa Nama'
|
||||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||||
@@ -357,15 +307,15 @@ function nodeTemplate(node: any) {
|
|||||||
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}>
|
||||||
@@ -374,19 +324,17 @@ function nodeTemplate(node: any) {
|
|||||||
<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="Kembali ke struktur organisasi" withArrow position="bottom">
|
<Tooltip label="Lihat Detail" withArrow position="bottom">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
size="xs"
|
size="xs"
|
||||||
mt="md"
|
mt="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const id = node?.data?.positionId
|
const id = node?.data?.id
|
||||||
if (id && (window as any).scrollTo) {
|
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
|
||||||
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Kembali
|
Lihat Detail
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -394,6 +342,3 @@ function nodeTemplate(node: any) {
|
|||||||
</Transition>
|
</Transition>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function Page() {
|
|||||||
<Text
|
<Text
|
||||||
fz={{ base: 'md', md: 'lg' }}
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
lh={1.7}
|
lh={1.7}
|
||||||
ta="justify"
|
ta="center"
|
||||||
dangerouslySetInnerHTML={{ __html: item.visi }}
|
dangerouslySetInnerHTML={{ __html: item.visi }}
|
||||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||||
/>
|
/>
|
||||||
@@ -88,7 +88,7 @@ function Page() {
|
|||||||
<Text
|
<Text
|
||||||
fz={{ base: 'md', md: 'lg' }}
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
lh={1.7}
|
lh={1.7}
|
||||||
ta="justify"
|
ta="center"
|
||||||
dangerouslySetInnerHTML={{ __html: item.misi }}
|
dangerouslySetInnerHTML={{ __html: item.misi }}
|
||||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,63 @@
|
|||||||
'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';
|
||||||
|
|
||||||
|
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() {
|
||||||
return (
|
return (
|
||||||
@@ -64,31 +121,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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,67 @@
|
|||||||
|
import { useRef, useState, useEffect } from 'react';
|
||||||
import stateNav from "@/state/state-nav";
|
import stateNav from "@/state/state-nav";
|
||||||
import { Container, Stack, TextInput, Tooltip } from "@mantine/core";
|
import { Container, Stack, ActionIcon, Box } from "@mantine/core";
|
||||||
import { IconSearch } from "@tabler/icons-react";
|
import { IconX } from '@tabler/icons-react';
|
||||||
|
import GlobalSearch from "./globalSearch";
|
||||||
|
|
||||||
export function NavbarSearch() {
|
export function NavbarSearch() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Close when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Only close if clicking outside both the search input and results
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(target) &&
|
||||||
|
!target.closest('.search-result-item') // Add a class to your search result items
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
stateNav.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box
|
||||||
|
ref={containerRef}
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
>
|
||||||
<Container
|
<Container
|
||||||
w={{ base: "100%", md: "80%" }}
|
w={{ base: "100%", md: "80%" }}
|
||||||
fluid
|
fluid
|
||||||
py="xl"
|
py="xl"
|
||||||
onMouseLeave={stateNav.clear}
|
|
||||||
>
|
>
|
||||||
<Stack pt="xl">
|
<Stack pt="xl">
|
||||||
<Tooltip label="Type to search across the site" position="bottom-start" withArrow>
|
<Box style={{ position: 'relative' }}>
|
||||||
<TextInput
|
<GlobalSearch />
|
||||||
autoFocus
|
{isOpen && (
|
||||||
size="lg"
|
<ActionIcon
|
||||||
variant="filled"
|
onClick={() => {
|
||||||
radius="xl"
|
setIsOpen(false);
|
||||||
placeholder="Search anything..."
|
stateNav.clear();
|
||||||
leftSection={<IconSearch size={20} />}
|
}}
|
||||||
/>
|
style={{
|
||||||
</Tooltip>
|
position: 'absolute',
|
||||||
|
right: 10,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
zIndex: 1000
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,10 +86,15 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
align="center"
|
align="center"
|
||||||
p="xs"
|
p="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (item.href) {
|
||||||
router.push(item.href);
|
router.push(item.href);
|
||||||
stateNav.mobileOpen = false;
|
stateNav.mobileOpen = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: item.href ? "pointer" : "default",
|
||||||
|
opacity: item.href ? 1 : 0.8
|
||||||
}}
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
>
|
||||||
<Text c="dark.9" fw={600} fz="md">
|
<Text c="dark.9" fw={600} fz="md">
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|||||||
@@ -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,6 +20,7 @@ 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"]}>
|
||||||
@@ -47,7 +47,12 @@ 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={pathname === item.href ||
|
||||||
|
(item.children?.some(child => child.href === pathname))}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Tooltip label="Search content" position="bottom" withArrow>
|
<Tooltip label="Search content" position="bottom" withArrow>
|
||||||
@@ -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={pathname === 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: pathname === link.href ? theme.colors.blue[0] : 'transparent',
|
||||||
color: colors['blue-button'],
|
color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
|
||||||
fontWeight: 500,
|
fontWeight: pathname === link.href ? 600 : 500,
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
background: theme.colors.gray[8],
|
background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
|
||||||
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
144
src/app/darmasaba/_com/globalSearch.tsx
Normal file
144
src/app/darmasaba/_com/globalSearch.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
|
||||||
|
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
|
||||||
|
import { IconX } from '@tabler/icons-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
import getDetailUrl from './searchUrl';
|
||||||
|
|
||||||
|
export default function GlobalSearch() {
|
||||||
|
const snap = useSnapshot(searchState);
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
|
// buka popover saat ada query
|
||||||
|
useEffect(() => {
|
||||||
|
setOpened(!!snap.query);
|
||||||
|
}, [snap.query]);
|
||||||
|
|
||||||
|
// infinite scroll
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
|
||||||
|
if (bottom && !snap.loading) searchState.next();
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, [snap.loading]);
|
||||||
|
|
||||||
|
const handleSelect = async (e: React.MouseEvent, item: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const url = getDetailUrl(item);
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
// Immediately close the search dropdown
|
||||||
|
setOpened(false);
|
||||||
|
searchState.results = []; // Clear results immediately
|
||||||
|
searchState.loading = false;
|
||||||
|
|
||||||
|
// Use window.location for navigation to ensure full page reload
|
||||||
|
window.location.href = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pos="relative">
|
||||||
|
<Popover
|
||||||
|
opened={opened && !!snap.query}
|
||||||
|
onChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
// Clear search state when popover is closed
|
||||||
|
searchState.query = '';
|
||||||
|
searchState.results = [];
|
||||||
|
searchState.page = 1;
|
||||||
|
searchState.nextPage = null;
|
||||||
|
}
|
||||||
|
setOpened(isOpen);
|
||||||
|
}}
|
||||||
|
width="target"
|
||||||
|
position="bottom"
|
||||||
|
shadow="md"
|
||||||
|
withinPortal
|
||||||
|
radius="md"
|
||||||
|
zIndex={1000} // Add this line to ensure it appears above other elements
|
||||||
|
styles={{
|
||||||
|
dropdown: {
|
||||||
|
zIndex: 1000, // Add this to ensure the dropdown appears above other elements
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover.Target>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari apapun..."
|
||||||
|
value={snap.query}
|
||||||
|
onChange={(e) => {
|
||||||
|
searchState.query = e.currentTarget.value;
|
||||||
|
debouncedFetch();
|
||||||
|
}}
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
rightSection={
|
||||||
|
snap.query ? (
|
||||||
|
<IconX
|
||||||
|
size={16}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
searchState.query = '';
|
||||||
|
searchState.results = [];
|
||||||
|
searchState.page = 1;
|
||||||
|
searchState.nextPage = null;
|
||||||
|
setOpened(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Popover.Target>
|
||||||
|
|
||||||
|
<Popover.Dropdown
|
||||||
|
p={0}
|
||||||
|
style={{
|
||||||
|
maxHeight: 350,
|
||||||
|
overflowY: 'auto',
|
||||||
|
borderRadius: 12,
|
||||||
|
zIndex: 1000, // Add this line to ensure dropdown stays above other elements
|
||||||
|
position: 'relative', // Add this to contain child elements
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{snap.results.length > 0 ? (
|
||||||
|
snap.results.map((item, i) => (
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
p="sm"
|
||||||
|
className="search-result-item" // Add this class
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid #eee',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
position: 'relative', // Add this
|
||||||
|
zIndex: 1, // Add this to ensure proper stacking context
|
||||||
|
backgroundColor: 'white', // Ensure background is set
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||||
|
onClick={(e) => handleSelect(e, item)} // Pass the event here
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{item.judul || item.namaPasar || item.nama || item.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
dari modul: {item.type}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Center py="md">
|
||||||
|
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -332,7 +332,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;
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -67,7 +65,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Tooltip>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,12 @@ export default function ProfileView({ data }: ProfileViewProps) {
|
|||||||
<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"
|
||||||
|
style={{
|
||||||
|
objectPosition: 'bottom center',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Stack align="center" gap="xs" w="100%" py="xl">
|
<Stack align="center" gap="xs" w="100%" py="xl">
|
||||||
@@ -49,13 +52,26 @@ 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 - sedikit overlap dengan gambar */}
|
||||||
|
<Box
|
||||||
|
pos="absolute"
|
||||||
|
bottom={-20} // bikin naik sedikit ke gambar
|
||||||
|
right={0}
|
||||||
|
w="100%"
|
||||||
|
p={{ base: 'xs', md: 'md' }}
|
||||||
|
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
|
||||||
|
>
|
||||||
<Card
|
<Card
|
||||||
px="lg"
|
px="lg"
|
||||||
radius="2xl"
|
py="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',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip label="Jabatan Resmi" withArrow>
|
<Tooltip label="Jabatan Resmi" withArrow>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dimmed">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function Penghargaan() {
|
|||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: "cyan", to: "blue", deg: 60 }}
|
gradient={{ from: "cyan", to: "blue", deg: 60 }}
|
||||||
>
|
>
|
||||||
Penghargaan & Prestasi Desa
|
Penghargaan Desa
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function SDGS() {
|
|||||||
</Title>
|
</Title>
|
||||||
</Center>
|
</Center>
|
||||||
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
|
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
|
||||||
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan: dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
|
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={50}>
|
||||||
@@ -78,6 +78,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 +108,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 +135,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" }} />
|
||||||
|
|||||||
36
src/app/darmasaba/_com/scrollToTopButton.tsx
Normal file
36
src/app/darmasaba/_com/scrollToTopButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client'
|
||||||
|
import { useWindowScroll } from '@mantine/hooks';
|
||||||
|
import { ActionIcon, Transition } from '@mantine/core';
|
||||||
|
import { IconArrowUp } from '@tabler/icons-react';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
|
function ScrollToTopButton() {
|
||||||
|
const [scroll, scrollTo] = useWindowScroll();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
mounted={scroll.y > 300}
|
||||||
|
transition="slide-up"
|
||||||
|
duration={300}
|
||||||
|
timingFunction="ease"
|
||||||
|
>
|
||||||
|
{(styles) => (
|
||||||
|
<ActionIcon
|
||||||
|
style={styles}
|
||||||
|
size="xl"
|
||||||
|
radius="xl"
|
||||||
|
variant="filled"
|
||||||
|
color={colors['blue-button']}
|
||||||
|
onClick={() => scrollTo({ y: 0 })}
|
||||||
|
pos="fixed"
|
||||||
|
bottom={24}
|
||||||
|
right={24}
|
||||||
|
aria-label="Kembali ke atas"
|
||||||
|
>
|
||||||
|
<IconArrowUp size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default ScrollToTopButton
|
||||||
91
src/app/darmasaba/_com/searchUrl.tsx
Normal file
91
src/app/darmasaba/_com/searchUrl.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
||||||
|
const { type, id, kategori } = item;
|
||||||
|
const typeUrlMap: Record<string, string> = {
|
||||||
|
programinovasi: `/darmasaba/program-inovasi/${id}`,
|
||||||
|
desaantikorupsi: '/darmasaba/desa-anti-korupsi',
|
||||||
|
sdgsdesa: '/darmasaba/sdgs-desa',
|
||||||
|
apbdes: '/darmasaba/apbdes',
|
||||||
|
prestasidesa: '/darmasaba/prestasi-desa',
|
||||||
|
pejabatdesa: '/darmasaba/profile/pejabat-desa',
|
||||||
|
strukturppid: '/darmasaba/ppid/struktur-ppid',
|
||||||
|
visimisippid: '/darmasaba/ppid/visi-misi',
|
||||||
|
dasarhukumppid: '/darmasaba/ppid/dasar-hukum',
|
||||||
|
profileppid: '/darmasaba/ppid/profile',
|
||||||
|
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik',
|
||||||
|
perbekeldarmasaba: '/darmasaba/desa/profile',
|
||||||
|
berita: `/darmasaba/desa/berita/${kategori}/${id}`,
|
||||||
|
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`,
|
||||||
|
sejarahdesa: '/darmasaba/desa/profile',
|
||||||
|
visimisidesa: '/darmasaba/desa/profile',
|
||||||
|
lambangdesa: '/darmasaba/desa/profile',
|
||||||
|
maskotdesa: '/darmasaba/desa/profile',
|
||||||
|
profilperbekel: '/darmasaba/desa/profile',
|
||||||
|
potensi: '/darmasaba/desa/potensi-desa',
|
||||||
|
galleryFoto: '/darmasaba/desa/gallery/foto',
|
||||||
|
galleryVideo: '/darmasaba/desa/gallery/video',
|
||||||
|
pelayananSuratKeterangan: '/darmasaba/desa/layanan',
|
||||||
|
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan',
|
||||||
|
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan',
|
||||||
|
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan',
|
||||||
|
penghargaan: '/darmasaba/desa/penghargaan',
|
||||||
|
posyandu: '/darmasaba/kesehatan/posyandu',
|
||||||
|
fasilitasKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||||
|
jadwalKegiatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||||
|
artikelKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||||
|
puskesmas: '/darmasaba/kesehatan/puskesmas',
|
||||||
|
programKesehatan: '/darmasaba/kesehatan/program-kesehatan',
|
||||||
|
penangananDarurat: '/darmasaba/kesehatan/penanganan-darurat',
|
||||||
|
kontakDarurat: '/darmasaba/kesehatan/kontak-darurat',
|
||||||
|
infoWabahPenyakit: '/darmasaba/kesehatan/info-wabah-penyakit',
|
||||||
|
keamananLingkungan: '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
|
||||||
|
polsekTerdekat: '/darmasaba/keamanan/polsek-terdekat',
|
||||||
|
kontakDaruratKeamanan: '/darmasaba/keamanan/kontak-darurat',
|
||||||
|
pencegahanKriminalitas: '/darmasaba/keamanan/pencegahan-kriminalitas',
|
||||||
|
laporanPublik: '/darmasaba/keamanan/laporan-publik',
|
||||||
|
tipsKeamanan: '/darmasaba/keamanan/tips-keamanan',
|
||||||
|
pasarDesa: '/darmasaba/ekonomi/pasar-desa',
|
||||||
|
lowonganKerjaLokal: '/darmasaba/ekonomi/lowongan-kerja-lokal',
|
||||||
|
strukturOrganisasi: '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
|
||||||
|
jumlahPendudukUsiaKerjaYangMenganggurUsia: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||||
|
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||||
|
jumlahPendudukMiskin: '/darmasaba/ekonomi/jumlah-penduduk-miskin',
|
||||||
|
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
|
||||||
|
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
|
||||||
|
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
|
||||||
|
desaDigital: '/darmasaba/inovasi/desa-digital-smart-village',
|
||||||
|
programKreatif: '/darmasaba/inovasi/program-kreatif-desa',
|
||||||
|
kolaborasiInovasi: '/darmasaba/inovasi/kolaborasi-inovasi',
|
||||||
|
mitraKolaborasi: '/darmasaba/inovasi/kolaborasi-inovasi',
|
||||||
|
infoTekno: '/darmasaba/inovasi/info-teknologi-tepat-guna',
|
||||||
|
pengelolaanSampah: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
|
||||||
|
keteranganBankSampahTerdekat: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
|
||||||
|
programPenghijauan: '/darmasaba/lingkungan/program-penghijauan',
|
||||||
|
dataLingkunganDesa: '/darmasaba/lingkungan/data-lingkungan-desa',
|
||||||
|
gotongRoyong: '/darmasaba/lingkungan/gotong-royong',
|
||||||
|
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';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDetailUrl;
|
||||||
@@ -8,13 +8,15 @@ import colors from "@/con/colors";
|
|||||||
import SDGS from "./_com/main-page/sdgs";
|
import SDGS from "./_com/main-page/sdgs";
|
||||||
// import ApiFetch from "@/lib/api-fetch";
|
// import ApiFetch from "@/lib/api-fetch";
|
||||||
|
|
||||||
import { Stack } from "@mantine/core";
|
import { Box, Stack } from "@mantine/core";
|
||||||
import Apbdes from "./_com/main-page/apbdes";
|
import Apbdes from "./_com/main-page/apbdes";
|
||||||
import Prestasi from "./_com/main-page/prestasi";
|
import Prestasi from "./_com/main-page/prestasi";
|
||||||
|
import ScrollToTopButton from "./_com/scrollToTopButton";
|
||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
|
<Box>
|
||||||
<Stack bg={colors.grey[1]} gap={"4rem"}>
|
<Stack bg={colors.grey[1]} gap={"4rem"}>
|
||||||
<LandingPage />
|
<LandingPage />
|
||||||
<Penghargaan />
|
<Penghargaan />
|
||||||
@@ -26,5 +28,8 @@ export default function Page() {
|
|||||||
<Apbdes />
|
<Apbdes />
|
||||||
<Prestasi />
|
<Prestasi />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
{/* Tombol Scroll ke Atas */}
|
||||||
|
<ScrollToTopButton />
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
backdrop-filter: blur(40px);
|
backdrop-filter: blur(40px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 50;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "PPID",
|
name: "PPID",
|
||||||
href: "/darmasaba/ppid/profile-ppid",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "1.1",
|
id: "1.1",
|
||||||
@@ -51,7 +50,6 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "Desa",
|
name: "Desa",
|
||||||
href: "/darmasaba/desa/profile",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "2.1",
|
id: "2.1",
|
||||||
@@ -94,7 +92,6 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
name: "Kesehatan",
|
name: "Kesehatan",
|
||||||
href: "/darmasaba/kesehatan/posyandu",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "3.1",
|
id: "3.1",
|
||||||
@@ -136,7 +133,6 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "4",
|
id: "4",
|
||||||
name: "Keamanan",
|
name: "Keamanan",
|
||||||
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "4.1",
|
id: "4.1",
|
||||||
@@ -173,7 +169,6 @@ const navbarListMenu = [
|
|||||||
{
|
{
|
||||||
id: "5",
|
id: "5",
|
||||||
name: "Ekonomi",
|
name: "Ekonomi",
|
||||||
href: "/darmasaba/ekonomi/pasar-desa",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "5.1",
|
id: "5.1",
|
||||||
@@ -229,7 +224,6 @@ const navbarListMenu = [
|
|||||||
}, {
|
}, {
|
||||||
id: "6",
|
id: "6",
|
||||||
name: "Inovasi",
|
name: "Inovasi",
|
||||||
href: "/darmasaba/inovasi/desa-digital-smart-village",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "6.1",
|
id: "6.1",
|
||||||
@@ -266,7 +260,6 @@ const navbarListMenu = [
|
|||||||
}, {
|
}, {
|
||||||
id: "7",
|
id: "7",
|
||||||
name: "Lingkungan",
|
name: "Lingkungan",
|
||||||
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "7.1",
|
id: "7.1",
|
||||||
@@ -302,7 +295,6 @@ const navbarListMenu = [
|
|||||||
}, {
|
}, {
|
||||||
id: "8",
|
id: "8",
|
||||||
name: "Pendidikan",
|
name: "Pendidikan",
|
||||||
href: "/darmasaba/pendidikan/info-sekolah",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "8.1",
|
id: "8.1",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export type MenuItem = {
|
export type MenuItem = {
|
||||||
id: string,
|
id: string;
|
||||||
name: string,
|
name: string;
|
||||||
href: string,
|
href?: string;
|
||||||
children?: MenuItem[]
|
children?: MenuItem[];
|
||||||
}
|
} & (
|
||||||
|
{ href: string; children?: MenuItem[] } |
|
||||||
|
{ children: MenuItem[] }
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user