Compare commits

...

2 Commits

Author SHA1 Message Date
25000d0b0f PPID > Profile PPID
Desa >  Profile Visi Misi Desa
Keamanan >  Pencegahan Kriminalitas  Grid Kiri  Datanya  Mepet
Keamanan >  Laporan  Publik
Ekonomi  >  Sektor Unggulan Desa Coba tampilin 3  Aja
2026-02-04 16:59:49 +08:00
bbd52fb6f5 Fix Jam Operasional Kantor Desa
Fix Agar Token Seafile ga expired cuma 1 hari
2026-02-04 11:47:56 +08:00
20 changed files with 270 additions and 64 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -50,6 +50,7 @@
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
"async-mutex": "^0.5.0",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",

View File

@@ -60,3 +60,12 @@ export async function seedDataPerpustakaan() {
}
console.log("✅ Data perpustakaan seeded successfully");
}
if (import.meta.main) {
seedDataPerpustakaan()
.then(() => {
console.log("seed data perpustakaan success");
})
.catch((err) => {
console.log("gagal seed data perpustakaan", JSON.stringify(err));
});
}

View File

@@ -138,5 +138,61 @@
"deskripsi": "<p>Cerita edukatif yang mengenalkan sains kepada anak dengan bahasa sederhana</p>",
"kategoriId": "cmkqb11mc000104jibqh7bdzu",
"imageName": "G0iELZb2DhQDCCP5OdzJR-desktop.webp"
},
{
"id": "cml7fq776000104jscnj58sgm",
"judul": "Pedagogy of the Oppressed",
"deskripsi": "<p>Klasik pemikiran pendidikan kritis; menggali hubungan guru-murid dan peran pendidikan dalam pembebasan sosial</p>",
"kategoriId": "cmkqb11mc000104jibq97bdzu",
"imageName": "pendidikan-1.webp"
},
{
"id": "cml7fqurm000204js5p60hkym",
"judul": "The Courage to Teach",
"deskripsi": "<p>Tentang refleksi diri seorang pendidik; cocok untuk pengajar yang ingin lebih dari sekedar “metode mengajar”</p>",
"kategoriId": "cmkqb11mc000104jibq97bdzu",
"imageName": "pendidikan-2.webp"
},
{
"id": "cml7fqurm000204js5p60hkzn",
"judul": "A Brief History of Time",
"deskripsi": "<p>Penjelasan kosmologi yang terkenal dunia; sains kompleks dibahas dengan bahasa yang bisa dinikmati pembaca umum</p>",
"kategoriId": "cmkqb11mc000104jibqa7bdzu",
"imageName": "ilmiah-1.webp"
},
{
"id": "cml7fqurm000204js5p60hkao",
"judul": "The Selfish Gene",
"deskripsi": "<p>Membawa perspektif baru tentang evolusi melalui “gen” sebagai unit seleksi</p>",
"kategoriId": "cmkqb11mc000104jibqa7bdzu",
"imageName": "ilmiah-2.webp"
},
{
"id": "cml7fx09c000304jshams3xbg",
"judul": "A Little Life",
"deskripsi": "<p>Novel yang menggambarkan hidup seorang remaja yang mengalami kehidupan yang sangat sulit</p>",
"kategoriId": "cmkqb11mc000104jibqb7bdzu",
"imageName": "drama-1.webp"
},
{
"id": "cml7fx09c000304jshams3xch",
"judul": "Death of a Salesman",
"deskripsi": "<p>Drama teater klasik Amerika tentang harapan, keluarga, dan realitas hidup.</p>",
"kategoriId": "cmkqb11mc000104jibqb7bdzu",
"imageName": "drama-2.webp"
},
{
"id": "cml7fx09c000304jshams3xdi",
"judul": "How Not to Die",
"deskripsi": "<p>Panduan berbasis penelitian tentang pola makan untuk mencegah dan menangani penyakit.</p>",
"kategoriId": "cmkqb11mc000104jibqg7bdzu",
"imageName": "kesehatan-1.webp"
},
{
"id": "cml7fx09c000304jshams3xej",
"judul": "The Body Keeps the Score",
"deskripsi": "<p>Fokus pada trauma, otak & tubuh; penting untuk memahami kesehatan mental secara mendalam.</p>",
"kategoriId": "cmkqb11mc000104jibqg7bdzu",
"imageName": "kesehatan-2.webp"
}
]

View File

@@ -1,3 +1,5 @@
import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
type DirItem = {
type: "file" | "dir";
name: string;
@@ -5,7 +7,6 @@ type DirItem = {
size?: number;
};
const TOKEN = process.env.SEAFILE_TOKEN!;
const REPO_ID = process.env.SEAFILE_REPO_ID!;
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
@@ -13,11 +14,12 @@ const DIR_TARGET = "asset-web";
const BASE_URL = process.env.SEAFILE_URL;
const headers = {
Authorization: `Token ${TOKEN}`,
};
async function getDirItems(): Promise<DirItem[]> {
const token = await getValidAuthToken();
const headers = {
Authorization: `Token ${token}`,
};
const res = await fetch(`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`, {
headers,
});
@@ -30,6 +32,11 @@ async function getDirItems(): Promise<DirItem[]> {
}
async function getDownloadUrl(filePath: string): Promise<string> {
const token = await getValidAuthToken();
const headers = {
Authorization: `Token ${token}`,
};
const res = await fetch(
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}&reuse=1`,
{ headers },

View File

@@ -38,12 +38,12 @@ export default async function seedAssets() {
console.log("🎉 Image seeding completed");
}
// if (import.meta.main) {
// seedAssets()
// .then(() => {
// console.log("seed assets success");
// })
// .catch((err) => {
// console.log("gagal seed assets", JSON.stringify(err));
// });
// }
if (import.meta.main) {
seedAssets()
.then(() => {
console.log("seed assets success");
})
.catch((err) => {
console.log("gagal seed assets", JSON.stringify(err));
});
}

View File

@@ -20,12 +20,25 @@ export default async function profilePerbekelFindById(request: Request) {
}, { status: 400 });
}
const data = await prisma.profilPerbekel.findUnique({
let data;
// Special handling for 'edit' - get the first/only record
if (id === 'edit') {
data = await prisma.profilPerbekel.findFirst({
where: { isActive: true },
include: {
image: true,
},
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
} else {
data = await prisma.profilPerbekel.findUnique({
where: { id },
include: {
image: true,
image: true,
}
});
});
}
if (!data) {
return Response.json({

View File

@@ -26,9 +26,19 @@ export default async function lambangDesaFindById(request: Request) {
);
}
const data = await prisma.lambangDesa.findUnique({
where: { id },
});
let data;
// Special handling for 'edit' - get the first/only record
if (id === 'edit') {
data = await prisma.lambangDesa.findFirst({
where: { isActive: true },
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
} else {
data = await prisma.lambangDesa.findUnique({
where: { id },
});
}
if (!data) {
return Response.json(

View File

@@ -4,7 +4,7 @@ export default async function maskotDesaFindById(request: Request){
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
@@ -20,16 +20,33 @@ export default async function maskotDesaFindById(request: Request){
}, {status: 400})
}
const data = await prisma.maskotDesa.findUnique({
where: { id },
include: {
images: {
include: {
image: true,
let data;
// Special handling for 'edit' - get the first/only record
if (id === 'edit') {
data = await prisma.maskotDesa.findFirst({
where: { isActive: true },
include: {
images: {
include: {
image: true,
}
}
},
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
} else {
data = await prisma.maskotDesa.findUnique({
where: { id },
include: {
images: {
include: {
image: true,
}
}
}
}
})
})
}
if(!data) {
return Response.json({

View File

@@ -4,7 +4,7 @@ export default async function sejarahDesaFindById(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
@@ -20,9 +20,19 @@ export default async function sejarahDesaFindById(request: Request) {
}, {status: 400})
}
const data = await prisma.sejarahDesa.findUnique({
where: { id },
})
let data;
// Special handling for 'edit' - get the first/only record
if (id === 'edit') {
data = await prisma.sejarahDesa.findFirst({
where: { isActive: true },
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
} else {
data = await prisma.sejarahDesa.findUnique({
where: { id },
})
}
if (!data) {
return Response.json({

View File

@@ -4,7 +4,7 @@ export default async function visiMisiDesaFindById(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
@@ -20,9 +20,19 @@ export default async function visiMisiDesaFindById(request: Request) {
}, {status: 400})
}
const data = await prisma.visiMisiDesa.findUnique({
where: { id },
})
let data;
// Special handling for 'edit' - get the first/only record
if (id === 'edit') {
data = await prisma.visiMisiDesa.findFirst({
where: { isActive: true },
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
} else {
data = await prisma.visiMisiDesa.findUnique({
where: { id },
})
}
if (!data) {
return Response.json({

View File

@@ -20,12 +20,25 @@ export default async function handler(request: Request) {
}, { status: 400 });
}
const data = await prisma.profilePPID.findUnique({
where: { id },
include: {
image: true,
}
});
let data;
// Special handling for 'edit' - get the first/only record
if (id === 'edit') {
data = await prisma.profilePPID.findFirst({
where: { isActive: true },
include: {
image: true,
},
orderBy: { createdAt: 'asc' } // Get the oldest one first
});
} else {
data = await prisma.profilePPID.findUnique({
where: { id },
include: {
image: true,
}
});
}
if (!data) {
return Response.json({

View File

@@ -1,7 +1,16 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, Paper, Skeleton, Center, Title } from '@mantine/core';
import React from 'react';
import {
Stack,
Box,
Text,
Paper,
Skeleton,
Center,
Title,
Pagination
} from '@mantine/core';
import React, { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts';
import { useProxy } from 'valtio/utils';
@@ -13,11 +22,15 @@ function Page() {
const {
data,
loading,
loading
} = state.findMany
const [activePage, setActivePage] = useState(1);
const itemsPerPage = 3;
useShallowEffect(() => {
state.findMany.load()
// Muat semua data tanpa batasan jumlah per halaman
state.findMany.load() // Ambil banyak data sekaligus
}, [])
if (loading || !data) {
@@ -44,6 +57,11 @@ function Page() {
);
}
// Filter data untuk halaman saat ini
const startIndex = (activePage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentData = data.slice(startIndex, endIndex);
const chartData = data
.filter(item => item?.name && typeof item.value === 'number')
.map((item) => ({
@@ -74,9 +92,9 @@ function Page() {
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg" justify="center">
{data.map((v, k) => {
{currentData.map((v, k) => {
return (
<Paper p="xl" key={k}>
<Paper p="xl" key={`${startIndex + k}`}>
<Title order={3} fw="bold">
{v.name}
</Title>
@@ -90,6 +108,18 @@ function Page() {
</Paper>
);
})}
{/* Pagination */}
<Center mt="xl">
<Pagination
total={Math.ceil(data.length / itemsPerPage)}
value={activePage}
onChange={setActivePage}
size="lg"
radius="md"
/>
</Center>
<Box style={{ width: '100%', overflowX: 'auto' }}>
<Paper p="xl">
<Title order={3} fw="bold" pb="md">

View File

@@ -162,6 +162,9 @@ function Page() {
p="lg"
shadow="sm"
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(0,0,0,0.1)',
@@ -169,7 +172,7 @@ function Page() {
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}}
>
<Stack gap="sm">
<Stack gap="sm" style={{ flex: 1 }}>
<Text
c={colors['blue-button']}
lineClamp={2}
@@ -190,7 +193,7 @@ function Page() {
: '-'}
</Text>
<Box>
<Box style={{ flex: 1 }}>
<Text fw="bold" fz="sm">
Penanganan:
</Text>
@@ -257,6 +260,7 @@ function Page() {
onClick={() => router.push(`/darmasaba/keamanan/laporan-publik/${v.id}`)}
size={mobile ? 'sm' : 'md'}
fullWidth
style={{ marginTop: 'auto' }}
>
{mobile ? 'Detail' : 'Lihat Detail Kronologi'}
</Button>

View File

@@ -55,9 +55,9 @@ function Page() {
<SimpleGrid
px={{ base: 20, md: 100 }}
cols={{ base: 1, md: 2 }}
spacing="xl"
spacing={{ base: 'md', md: 'xl' }}
>
<Paper p="xl" radius="xl" shadow="lg" >
<Paper p="xl" radius="xl" shadow="lg" bg="white">
<Title order={2} c={colors['blue-button']} fw="bold" lh={1.2}>
Program Keamanan Berjalan
</Title>
@@ -85,9 +85,9 @@ function Page() {
<SimpleGrid
px={{ base: 20, md: 100 }}
cols={{ base: 1, md: 2 }}
spacing="xl"
spacing={{ base: 'md', md: 'xl' }}
>
<Paper p="xl" radius="xl" shadow="lg" >
<Paper p="xl" radius="xl" shadow="lg" bg="white">
<Title order={2} c={colors['blue-button']} fw="bold" lh={1.2}>
Program Keamanan Berjalan
</Title>
@@ -108,6 +108,7 @@ function Page() {
cursor: 'pointer',
backgroundColor: colors['blue-button'],
transition: 'all 0.2s ease',
marginBottom: '10px' // Add space between items
}}
onClick={() =>
router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)
@@ -160,7 +161,7 @@ function Page() {
{findFirst.loading ? (
<Center><Skeleton h={400} /></Center>
) : findFirst.data ? (
<Paper p="xl" radius="xl" shadow="lg">
<Paper p="xl" radius="xl" shadow="lg" bg="white">
{findFirst.data?.linkVideo ? (
<Box
component="iframe"
@@ -168,7 +169,7 @@ function Page() {
width="100%"
height={300}
allowFullScreen
style={{ borderRadius: 8 }}
style={{ borderRadius: 8, marginBottom: 15 }}
/>
) : (
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>

View File

@@ -19,19 +19,27 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
const searchQuery = searchParams.get('search') || '';
const router = useTransitionRouter()
// Convert kebab-case back to original category name format
// This reverses the transformation done in layoutTabs: item.name.toLowerCase().replace(/\s+/g, '-')
const convertKebabCaseToOriginal = (kebabStr: string): string => {
// Replace hyphens with spaces
return kebabStr.replace(/-/g, ' ');
};
const decodedKategoriBuku = decodeURIComponent(kategoriBuku);
const originalKategoriName = convertKebabCaseToOriginal(decodedKategoriBuku);
const loadData = useCallback(async (searchQuery: string = '', page: number = 1) => {
try {
setIsLoading(true);
const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : originalKategoriName;
await state.dataPerpustakaan.findMany.load(page, 3, searchQuery, currentKategoriFilter);
setCurrentPage(page);
setTotalPages(state.dataPerpustakaan.findMany.totalPages);
} finally {
setIsLoading(false);
}
}, [state.dataPerpustakaan.findMany, decodedKategoriBuku]);
}, [state.dataPerpustakaan.findMany, originalKategoriName, decodedKategoriBuku]);
useShallowEffect(() => {
loadData(searchQuery);

View File

@@ -123,7 +123,7 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
let workHoursMessage = "";
if (["Senin", "Selasa", "Rabu", "Kamis"].includes(day)) {
workHoursMessage = "07:30 - 15:10";
workHoursMessage = "07:30 - 15:30";
} else if (day === "Jumat") {
workHoursMessage = "07:30 - 12:00";
}

View File

@@ -0,0 +1,17 @@
// Function to get the static authentication token from environment variables
export async function getValidAuthToken(): Promise<string> {
const staticToken = process.env.SEAFILE_TOKEN;
if (!staticToken) {
throw new Error('SEAFILE_TOKEN environment variable is not set');
}
console.log("Using static SEAFILE_TOKEN from environment variables");
return staticToken;
}
// Function to check if the token is set (always true since we're using static token)
export function isTokenValid(): boolean {
return !!process.env.SEAFILE_TOKEN;
}

2
x.json
View File

@@ -66,4 +66,4 @@
"version": 1,
"salt": ""
}
]
]

6
x.sh
View File

@@ -1,8 +1,8 @@
# ambil token
# curl -X POST https://cld-dkr-makuro-seafile.wibudev.com/api2/auth-token/ \
# -d "username=nico@bip.com" \
# -d "password=Production_123"
curl -X POST https://cld-dkr-makuro-seafile.wibudev.com/api2/auth-token/ \
-d "username=nico@bip.com" \
-d "password=Production_123"
# ambil list repo / library