Merge branch 'staging'

This commit is contained in:
bipproduction
2025-02-20 15:32:00 +08:00
86 changed files with 1746 additions and 489 deletions

62
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: CI Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
services:
# Menjalankan PostgreSQL sebagai service di GitHub Actions
postgres:
image: postgres:14
env:
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
# Checkout kode sumber
- name: Checkout code
uses: actions/checkout@v3
# Setup Bun
- name: Setup Bun
uses: oven-sh/setup-bun@v1
# Install dependencies
- name: Install dependencies
run: bun install
# Konfigurasi environment variable untuk PostgreSQL dan variabel tambahan
- name: Set up environment variables
run: |
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
echo "PORT=${{ secrets.PORT }}" >> .env
echo "NEXT_PUBLIC_WIBU_URL=${{ secrets.NEXT_PUBLIC_WIBU_URL }}" >> .env
echo "WIBU_UPLOAD_DIR=${{ secrets.WIBU_UPLOAD_DIR }}" >> .env
# Migrasi database menggunakan Prisma
- name: Apply Prisma schema to database
run: bun prisma db push
# Seed database (opsional)
- name: Seed database
run: bun prisma db seed
# Build project
- name: Build project
run: bun run build

7
.gitignore vendored
View File

@@ -39,3 +39,10 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# uploads
/uploads
# cache
/cache

BIN
bun.lockb

Binary file not shown.

Binary file not shown.

68
find-port.ts Normal file
View File

@@ -0,0 +1,68 @@
import getPort, { portNumbers } from 'get-port';
/**
* Mencari port yang tersedia dalam rentang tertentu.
* @param params - Parameter opsional untuk mencari port.
* @param params.count - Jumlah port yang dibutuhkan (default: 1).
* @param params.portStart - Awal rentang port (default: 3000).
* @param params.portEnd - Akhir rentang port (default: 6000).
* @param params.exclude - Daftar port yang harus dikecualikan.
* @returns Array port yang tersedia atau null jika tidak ada port yang cukup.
*/
async function findPort(params?: { count?: number, portStart?: number, portEnd?: number, exclude?: number[] }) {
const { count = 1, portStart = 3000, portEnd = 6000, exclude = [] } = params || {};
// Gabungkan port yang dikecualikan
const listPort = [...exclude]; // Hapus .flat() karena tidak diperlukan
const usedPorts = Array.from(new Set(listPort)) as number[];
// Validasi input
if (count <= 0) {
throw new Error('Count harus lebih besar dari 0');
}
if (count > (portEnd - portStart + 1)) {
throw new Error(`Count tidak boleh lebih besar dari range port (${portEnd - portStart + 1})`);
}
if (portStart >= portEnd) {
throw new Error('portStart harus lebih kecil dari portEnd');
}
if (portStart < 0 || portEnd > 65535) {
throw new Error('Port harus berada dalam rentang 0-65535');
}
// Optimasi pencarian port
const availablePorts = new Set<number>();
const portRange = portNumbers(portStart, portEnd);
const usedPortsSet = new Set(usedPorts);
for (const port of portRange) {
if (availablePorts.size >= count) break;
// Skip jika port sudah digunakan
if (usedPortsSet.has(port)) continue;
try {
const availablePort = await getPort({
port,
exclude: [...usedPorts, ...Array.from(availablePorts)],
});
// Pastikan port yang diperiksa berada dalam rentang yang ditentukan
if (availablePort === port && availablePort >= portStart && availablePort <= portEnd) {
availablePorts.add(port);
}
} catch (error) {
console.warn(`Gagal memeriksa port ${port}:`, error);
continue; // Lanjutkan ke port berikutnya
}
}
// Jika tidak cukup port yang tersedia, lempar error
if (availablePorts.size < count) {
throw new Error('Tidak cukup port yang tersedia dalam rentang yang diberikan');
}
return Array.from(availablePorts);
}
export default findPort;

View File

@@ -15,26 +15,31 @@
"@elysiajs/swagger": "^1.2.0",
"@mantine/carousel": "^7.16.2",
"@mantine/core": "^7.16.2",
"@mantine/dropzone": "^7.17.0",
"@mantine/hooks": "^7.16.2",
"@paljs/types": "^8.1.0",
"@prisma/client": "^6.3.1",
"@tabler/icons-react": "^3.30.0",
"@types/bun": "^1.2.2",
"@types/lodash": "^4.17.15",
"add": "^2.0.6",
"animate.css": "^4.1.1",
"compress-pdf": "^0.5.2",
"bun": "^1.2.2",
"elysia": "^1.2.12",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"framer-motion": "^12.4.1",
"get-port": "^7.1.0",
"lodash": "^4.17.21",
"motion": "^12.4.1",
"nanoid": "^5.1.0",
"next": "15.1.6",
"next-view-transitions": "^0.3.4",
"p-limit": "^6.2.0",
"prisma": "^6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-multi-carousel": "^2.8.5",
"react-scroll-motion": "^0.3.5",
"react-simple-toasts": "^6.1.0",
"readdirp": "^4.1.1",
"swr": "^2.3.2",
"valtio": "^2.1.3"

View File

@@ -33,7 +33,16 @@ import prisma from '@/lib/prisma';
}
console.log("potensi success ...")
})().then(() => prisma.$disconnect()).catch((e) => {
console.error(e)
prisma.$disconnect()
});
process.on('exit', () => {
prisma.$disconnect()
})
})();
process.on('SIGINT', () => {
prisma.$disconnect()
process.exit(0)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/no-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,41 @@
"use client";
import colors from "@/con/colors";
import images from "@/con/images";
import { Flex, Image, Paper, Stack, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
export default function SpashScreen() {
useShallowEffect(() => {
const timeout = setTimeout(() => {
window.location.href = "/darmasaba";
}, 3000);
return () => clearTimeout(timeout);
}, []);
return (
<Stack
w={"100%"}
h={"100vh"}
justify="center"
align="center"
bg={colors["blue-button"]}
>
<Paper p={"md"} miw={320}>
<Flex>
<Image
src={images["darmasaba-icon"]}
alt="darmasaba"
w={100}
h={100}
/>
<Stack p={"md"} gap={"0"}>
<Text>Pemerintah Desa</Text>
<Text c={colors["blue-button"]} fz={"2rem"} fw={"bold"}>
DARMASABA
</Text>
</Stack>
</Flex>
</Paper>
</Stack>
);
}

View File

@@ -0,0 +1,18 @@
"use client";
import { Button, Stack } from "@mantine/core";
import { Link } from "next-view-transitions";
export default function AdminNav() {
return (
<Stack>
<Button.Group p={"md"}>
<Button component={Link} href="/admin/images">
Images
</Button>
<Button component={Link} href="/admin/csv">
CSV
</Button>
</Button.Group>
</Stack>
);
}

View File

@@ -0,0 +1,139 @@
"use client";
import stateListImage from "@/state/state-list-image";
import {
ActionIcon,
Box,
Button,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconX } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { useState } from "react";
import toast from "react-simple-toasts";
import { useSnapshot } from "valtio";
export default function ListImage() {
const { list, total } = useSnapshot(stateListImage);
const [loading, setLoading] = useState(false);
useShallowEffect(() => {
// get url
console.log(window.location.origin);
stateListImage.load();
}, []);
let timeOut: NodeJS.Timer;
return (
<Stack p={"lg"}>
<Group justify="end">
<TextInput
leftSection={<IconSearch />}
rightSection={
<ActionIcon
variant="transparent"
onClick={() => {
stateListImage.load();
}}
>
<IconX />
</ActionIcon>
}
placeholder="Cari"
onChange={(e) => {
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => {
stateListImage.load({ search: e.target.value });
}, 200);
}}
/>
</Group>
<SimpleGrid
cols={{
base: 3,
md: 5,
lg: 10,
}}
>
{list &&
list.map((v, k) => {
return (
<Paper key={k} shadow="sm">
<Stack pos={"relative"} gap={0} justify="space-between">
<motion.div
onClick={() => {
// copy to clipboard
navigator.clipboard.writeText(v.url);
toast("Berhasil disalin");
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
<Image
h={100}
src={v.url + "?size=100"}
alt={v.name}
fit="cover"
loading="lazy"
style={{
objectFit: "cover",
objectPosition: "center",
}}
/>
</motion.div>
<Box p={"md"} h={54}>
<Text lineClamp={2} fz={"xs"}>
{v.name}
</Text>
</Box>
<Group justify="end">
<Button.Group>
<Button
onClick={() => {
// copy to clipboard
navigator.clipboard.writeText(v.url);
toast("Berhasil disalin");
}}
variant="subtle"
size="compact-xs"
>
Copy
</Button>
<Button
variant="subtle"
loading={loading}
size="compact-xs"
onClick={() => {
stateListImage.del({ name: v.name }).finally(() => {
setLoading(false);
});
}}
>
delete
</Button>
</Button.Group>
</Group>
</Stack>
</Paper>
);
})}
</SimpleGrid>
{total && (
<Pagination
total={total}
onChange={(e) => {
stateListImage.page = e;
stateListImage.load();
}}
/>
)}
</Stack>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import ApiFetch from "@/lib/api-fetch";
import { Group, Stack, Text } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { useState } from "react";
import toast from "react-simple-toasts";
export default function UploadCsv() {
return (
<Stack p={"md"}>
<DropUpload />
</Stack>
);
}
function DropUpload() {
const [loading, setLoading] = useState(false);
return (
<Stack justify="center" align="center">
<Group>
<Dropzone
loading={loading}
miw={460}
// accept csv
accept={["text/csv"]}
onDrop={async (droppedFiles) => {
if (droppedFiles.length < 0) {
return toast("Tidak ada file yang diunggah");
}
setLoading(true);
for (const file of droppedFiles) {
await ApiFetch.api["upl-csv-single"].post({
name: file.name,
file,
});
}
setLoading(false);
}}
>
<Stack>
<Text ta="center">Drop Csv here</Text>
</Stack>
</Dropzone>
</Group>
</Stack>
);
}

View File

@@ -0,0 +1,34 @@
"use client";
import ApiFetch from "@/lib/api-fetch";
import stateListImage from "@/state/state-list-image";
import { Stack, Text } from "@mantine/core";
import { Dropzone, IMAGE_MIME_TYPE } from "@mantine/dropzone";
import { useState } from "react";
export default function UploadImage() {
const [loading, setLoading] = useState(false);
return (
<Stack p={"md"}>
<Dropzone
loading={loading}
accept={IMAGE_MIME_TYPE} // Hanya menerima tipe file gambar
onDrop={async (droppedFiles) => {
setLoading(true);
for (const file of droppedFiles) {
await ApiFetch.api["upl-img-single"].post({
file: file,
name: file.name,
});
}
setLoading(false);
stateListImage.load();
}}
>
<Stack align="center">
<Text ta="center">Drop images here</Text>
</Stack>
</Dropzone>
</Stack>
);
}

View File

@@ -0,0 +1,10 @@
import { Stack } from "@mantine/core";
import UploadCsv from "../_com/UploadCsv";
export default function Page() {
return (
<Stack>
<UploadCsv />
</Stack>
);
}

View File

@@ -0,0 +1,12 @@
import { Stack } from "@mantine/core";
import ListImage from "../_com/ListImage";
import UploadImage from "../_com/UploadImage";
export default function Page() {
return (
<Stack>
<UploadImage />
<ListImage />
</Stack>
);
}

11
src/app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Stack } from "@mantine/core";
import AdminNav from "./_com/AdminNav";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<Stack gap={0}>
<AdminNav />
{children}
</Stack>
);
}

9
src/app/admin/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { Container, Stack } from "@mantine/core";
export default function Page() {
return (
<Stack h={"100%"}>
<Container>admin</Container>
</Stack>
);
}

View File

@@ -1,128 +0,0 @@
"use client"
import {
motion,
MotionValue,
useScroll,
useSpring,
useTransform,
} from "motion/react"
import { useRef } from "react"
function useParallax(value: MotionValue<number>, distance: number) {
return useTransform(value, [0, 1], [-distance, distance])
}
function Image({ id }: { id: number }) {
const ref = useRef(null)
const { scrollYProgress } = useScroll({ target: ref })
const y = useParallax(scrollYProgress, 300)
return (
<section className="img-container">
<div ref={ref}>
<img
src={`https://placehold.co/40${id}`}
alt="A London skyscraper"
/>
</div>
<motion.h2
// Hide until scroll progress is measured
initial={{ visibility: "hidden" }}
animate={{ visibility: "visible" }}
style={{ y }}
>{`#00${id}`}</motion.h2>
</section>
)
}
export default function Parallax() {
const { scrollYProgress } = useScroll()
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
})
return (
<div id="example">
{[1, 2, 3, 4, 5].map((image) => (
<Image key={image} id={image} />
))}
<motion.div className="progress" style={{ scaleX }} />
<StyleSheet />
</div>
)
}
/**
* ============== Styles ================
*/
function StyleSheet() {
return (
<style>{`
html {
scroll-snap-type: y mandatory;
}
.img-container {
height: 100vh;
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.img-container > div {
width: 300px;
height: 400px;
margin: 20px;
background: #f5f5f5;
overflow: hidden;
}
.img-container img {
width: 300px;
height: 400px;
}
@media (max-width: 500px) {
.img-container > div {
width: 150px;
height: 200px;
}
.img-container img {
width: 150px;
height: 200px;
}
}
.img-container h2 {
color: #4ff0b7;
margin: 0;
font-family: JetBrains Mono, monospace;
font-size: 50px;
font-weight: 700;
letter-spacing: -3px;
line-height: 1.2;
position: absolute;
display: inline-block;
top: calc(50% - 25px);
left: calc(50% + 120px);
}
.progress {
position: fixed;
left: 0;
right: 0;
height: 5px;
background: #4ff0b7;
bottom: 50px;
transform: scaleX(0);
}
`}</style>
)
}

View File

@@ -0,0 +1,20 @@
import fs from "fs/promises";
import path from "path";
async function imgDel({
name,
UPLOAD_DIR_IMAGE,
}: {
name: string;
UPLOAD_DIR_IMAGE: string;
}) {
try {
await fs.unlink(path.join(UPLOAD_DIR_IMAGE, name));
return "ok";
} catch (error) {
console.log(error);
return "error";
}
}
export default imgDel;

View File

@@ -0,0 +1,61 @@
import fs from "fs/promises";
import path from "path";
import sharp from "sharp";
async function img({
name,
UPLOAD_DIR_IMAGE,
ROOT,
size,
}: {
name: string;
UPLOAD_DIR_IMAGE: string;
ROOT: string;
size?: number; // Ukuran opsional (tidak ada default)
}) {
const completeName = path.basename(name); // Nama file lengkap
const ext = path.extname(name).toLowerCase(); // Ekstensi file dalam huruf kecil
// const fileNameWithoutExt = path.basename(name, ext); // Nama file tanpa ekstensi
// Default image jika terjadi kesalahan
const noImage = path.join(ROOT, "public/no-image.jpg");
// Validasi ekstensi file
if (![".jpg", ".jpeg", ".png"].includes(ext)) {
console.warn(`Ekstensi file tidak didukung: ${ext}`);
return new Response(await fs.readFile(noImage), {
headers: { "Content-Type": "image/jpeg" },
});
}
try {
// Path ke file asli
const filePath = path.join(UPLOAD_DIR_IMAGE, completeName);
// Periksa apakah file ada
await fs.stat(filePath);
// Metadata gambar asli
const metadata = await sharp(filePath).metadata();
// Proses resize menggunakan sharp
const resizedImageBuffer = await sharp(filePath)
.resize(size || metadata.width) // Gunakan size jika diberikan, jika tidak gunakan width asli
.toBuffer();
return new Response(resizedImageBuffer, {
headers: {
"Cache-Control": "public, max-age=3600, stale-while-revalidate=600",
"Content-Type": "image/jpeg",
},
});
} catch (error) {
console.error(`Gagal memproses file: ${name}`, error);
// Jika file tidak ditemukan atau gagal diproses, kembalikan default image
return new Response(await fs.readFile(noImage), {
headers: { "Content-Type": "image/jpeg" },
});
}
}
export default img;

View File

@@ -0,0 +1,30 @@
import fs from "fs/promises";
async function imgs({
search = "",
page = 1,
count = 20,
UPLOAD_DIR_IMAGE,
}: {
search?: string;
page?: number;
count?: number;
UPLOAD_DIR_IMAGE: string;
}) {
const files = await fs.readdir(UPLOAD_DIR_IMAGE);
return files
.filter(
(file) =>
file.endsWith(".jpg") || file.endsWith(".png") || file.endsWith(".jpeg")
)
.filter((file) => file.includes(search))
.slice((page - 1) * count, page * count)
.map((file) => ({
name: file,
url: `/api/img/${file}`,
total: files.length,
}));
}
export default imgs;

View File

@@ -0,0 +1,12 @@
export async function uplCsvSingle({
fileName,
file,
}: {
fileName: string;
file: File;
}) {
const textFile = await file.text();
console.log(fileName, textFile);
return "ok";
}

View File

@@ -0,0 +1,16 @@
async function uplCsv({ files }: { files: File[] }) {
if (!Array.isArray(files) || files.length === 0) {
throw new Error("Tidak ada file yang diunggah");
}
for (const file of files) {
const textFile = await file.text();
const fileName = file.name;
console.log(textFile, fileName);
}
return "ok";
}
export default uplCsv;

View File

@@ -0,0 +1,32 @@
import { nanoid } from "nanoid";
import fs from "fs/promises";
import path from "path";
import _ from "lodash";
export async function uplImgSingle({
fileName,
file,
UPLOAD_DIR_IMAGE,
}: {
fileName: string;
file: File;
UPLOAD_DIR_IMAGE: string;
}) {
if (!fileName || typeof fileName !== "string" || fileName.trim() === "") {
console.warn(`Nama file tidak valid: ${fileName}`);
fileName = nanoid() + ".jpg";
}
const ext = path.extname(fileName).toLowerCase();
const fileNameWithoutExt = path.basename(fileName, ext);
const fileNameKebabCase = _.kebabCase(fileNameWithoutExt) + ext;
try {
const buffer = Buffer.from(await file.arrayBuffer());
const filePath = path.join(UPLOAD_DIR_IMAGE, fileNameKebabCase);
await fs.writeFile(filePath, buffer);
return filePath;
} catch (error) {
console.log(error);
return "error";
}
}

View File

@@ -0,0 +1,52 @@
import path from "path";
import fs from "fs/promises";
import { nanoid } from "nanoid";
async function uplImg({
files,
UPLOAD_DIR_IMAGE,
}: {
files: File[];
UPLOAD_DIR_IMAGE: string;
}) {
// Validasi input
if (!Array.isArray(files) || files.length === 0) {
throw new Error("Tidak ada file yang diunggah");
}
for (const file of files) {
let fileName = file.name;
// Validasi nama file
if (!fileName || typeof fileName !== "string" || fileName.trim() === "") {
console.warn(`Nama file tidak valid: ${fileName}`);
fileName = nanoid() + ".jpg";
}
// Sanitasi nama file untuk mencegah path traversal
const sanitizedFileName = sanitizeFileName(fileName);
try {
// Konversi file ke buffer
const buffer = Buffer.from(await file.arrayBuffer());
// Tulis file ke direktori uploads
const filePath = path.join(UPLOAD_DIR_IMAGE, sanitizedFileName);
await fs.writeFile(filePath, buffer);
console.log(`File berhasil diunggah: ${sanitizedFileName}`);
} catch (error) {
console.error(`Gagal mengunggah file ${fileName}:`, error);
throw new Error(`Gagal mengunggah file: ${fileName}`);
}
}
return "ok";
}
// Fungsi untuk membersihkan nama file dari karakter yang tidak aman
function sanitizeFileName(fileName: string): string {
return fileName.replace(/[^a-zA-Z0-9._\-]/g, "_");
}
export default uplImg;

View File

@@ -1,32 +1,171 @@
import prisma from "@/lib/prisma";
import cors, { HTTPMethod } from "@elysiajs/cors";
import swagger from "@elysiajs/swagger";
import { Elysia } from "elysia";
import { Elysia, t } from "elysia";
import getPotensi from "./_lib/get-potensi";
import img from "./_lib/img";
import fs from "fs/promises";
import path from "path";
import uplImg from "./_lib/upl-img";
import imgs from "./_lib/imgs";
import uplCsv from "./_lib/upl-csv";
import imgDel from "./_lib/img-del";
import { uplImgSingle } from "./_lib/upl-img-single";
import { uplCsvSingle } from "./_lib/upl-csv-single";
const ROOT = process.cwd();
if (!process.env.WIBU_UPLOAD_DIR)
throw new Error("WIBU_UPLOAD_DIR is not defined");
const UPLOAD_DIR = path.join(ROOT, process.env.WIBU_UPLOAD_DIR);
const UPLOAD_DIR_IMAGE = path.join(UPLOAD_DIR, "image");
// create uploads dir
fs.mkdir(UPLOAD_DIR, {
recursive: true,
}).catch(() => {});
// create image uploads dir
fs.mkdir(UPLOAD_DIR_IMAGE, {
recursive: true,
}).catch(() => {});
const corsConfig = {
origin: "*",
methods: ["GET", "POST", "PATCH", "DELETE", "PUT"] as HTTPMethod[],
allowedHeaders: "*",
exposedHeaders: "*",
maxAge: 5,
credentials: true,
origin: "*",
methods: ["GET", "POST", "PATCH", "DELETE", "PUT"] as HTTPMethod[],
allowedHeaders: "*",
exposedHeaders: "*",
maxAge: 5,
credentials: true,
};
async function layanan() {
const data = await prisma.layanan.findMany();
return { data };
const data = await prisma.layanan.findMany();
return { data };
}
const ApiServer = new Elysia()
.use(swagger({ path: "/api/docs" }))
.use(cors(corsConfig))
.group("/api", app => app
.get("/layanan", layanan)
.get("/potensi", getPotensi)
)
.use(swagger({ path: "/api/docs" }))
.use(cors(corsConfig))
.onError(({ code }) => {
if (code === "NOT_FOUND") {
return {
status: 404,
body: "Route not found :(",
};
}
})
.group("/api", (app) =>
app
.get("/layanan", layanan)
.get("/potensi", getPotensi)
.get(
"/img/:name",
({ params, query }) => {
return img({
name: params.name,
UPLOAD_DIR_IMAGE,
ROOT,
size: query.size,
});
},
{
params: t.Object({
name: t.String(),
}),
query: t.Optional(
t.Object({
size: t.Optional(t.Number()),
})
),
}
)
.delete(
"/img/:name",
({ params }) => {
return imgDel({
name: params.name,
UPLOAD_DIR_IMAGE,
});
},
{
params: t.Object({
name: t.String(),
}),
}
)
.get(
"/imgs",
({ query }) => {
return imgs({
search: query.search,
page: query.page,
count: query.count,
UPLOAD_DIR_IMAGE,
});
},
{
query: t.Optional(
t.Object({
page: t.Number({ default: 1 }),
count: t.Number({ default: 10 }),
search: t.String({ default: "" }),
})
),
}
)
.post(
"/upl-img",
({ body }) => {
console.log(body.title);
return uplImg({ files: body.files, UPLOAD_DIR_IMAGE });
},
{
body: t.Object({
title: t.String(),
files: t.Files({ multiple: true }),
}),
}
)
.post(
"/upl-img-single",
({ body }) => {
return uplImgSingle({
fileName: body.name,
file: body.file,
UPLOAD_DIR_IMAGE,
});
},
{
body: t.Object({
name: t.String(),
file: t.File(),
}),
}
)
.post(
"/upl-csv-single",
({ body }) => {
return uplCsvSingle({ fileName: body.name, file: body.file });
},
{
body: t.Object({
name: t.String(),
file: t.File(),
}),
}
)
.post(
"/upl-csv",
({ body }) => {
return uplCsv({ files: body.files });
},
{
body: t.Object({
files: t.Files(),
}),
}
)
);
export const GET = ApiServer.handle;
export const POST = ApiServer.handle;
@@ -34,5 +173,4 @@ export const PATCH = ApiServer.handle;
export const DELETE = ApiServer.handle;
export const PUT = ApiServer.handle;
export type AppServer = typeof ApiServer
export type AppServer = typeof ApiServer;

View File

@@ -0,0 +1,8 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return <div>
{sub}
</div>
}

View File

@@ -0,0 +1,5 @@
export default function Page() {
return <div>
ekonomi
</div>
}

View File

@@ -0,0 +1,8 @@
import { Stack } from "@mantine/core";
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return <Stack>
{sub}
</Stack>
}

View File

@@ -0,0 +1,6 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return <div>
{sub}
</div>
}

View File

@@ -0,0 +1,8 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return (
<div>
<h1>{sub}</h1>
</div>
);
}

View File

@@ -0,0 +1,8 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return (
<div>
{sub}
</div>
);
}

View File

@@ -0,0 +1,8 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return <div>
{sub}
</div>
}

View File

@@ -0,0 +1,6 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return <div>
{sub}
</div>
}

View File

@@ -0,0 +1,8 @@
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return (
<div>
<h1>{sub}</h1>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { Stack } from "@mantine/core";
export default async function Page({ params }: { params: Promise<{ sub: string }> }) {
const { sub } = await params
return (
<Stack>
{sub}
</Stack>
)
}

View File

@@ -0,0 +1,14 @@
'use client'
import { ActionIcon } from "@mantine/core";
import { IconArrowLeft } from "@tabler/icons-react";
import { useTransitionRouter } from 'next-view-transitions';
export default function BackButton() {
const router = useTransitionRouter()
return (
<ActionIcon variant="transparent" onClick={() => router.back()}>
<IconArrowLeft />
</ActionIcon>
);
}

View File

@@ -2,6 +2,6 @@ import { Stack } from "@mantine/core";
export default function Page() {
return <Stack>
ekonomi
layanan
</Stack>
}

View File

@@ -0,0 +1,5 @@
export default function Page() {
return <div>
penghargaan
</div>
}

View File

@@ -1,8 +1,9 @@
import colors from "@/con/colors";
import { Stack, Container, Center, Text } from "@mantine/core";
function Footer() {
return <Stack>
return <Stack bg={colors["blue-button"]} c={colors["white-trans-1"]}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} h={720} >
<Center>
<Text fz={"3.4rem"}>Footer</Text>

View File

@@ -1,16 +1,21 @@
import { Space, Stack } from "@mantine/core";
import { Navbar } from "./Navbar";
import colors from "@/con/colors";
import { Box, Space, Stack } from "@mantine/core";
import Footer from "./Footer";
import { Navbar } from "./Navbar";
export function MainLayout({ children }: { children: React.ReactNode }) {
return (
<Stack gap={0}>
<Stack gap={0} bg={colors.grey[1]}>
<Navbar />
<Space h={{
base: "2.2rem",
md: "2.5rem"
}} />
{children}
<Box style={{
overflow: "scroll"
}}>
{children}
</Box>
<Footer />
</Stack>
)

View File

@@ -0,0 +1,75 @@
"use client";
import colors from "@/con/colors";
import navbarListMenu from "@/con/navbar-list-menu";
import stateNav from "@/state/state-nav";
import { ActionIcon, Box, Burger, Group, Stack, Text } from "@mantine/core";
import { IconHome, IconSquareArrowRight } from "@tabler/icons-react";
import { motion } from 'framer-motion';
import { useSnapshot } from "valtio";
import { MenuItem } from "../../../../types/menu-item";
import { NavbarMainMenu } from "./NavbarMainMenu";
import { useRouter } from 'next/navigation'
export function Navbar() {
const { item, isSearch, mobileOpen } = useSnapshot(stateNav);
const router = useRouter()
return (
<Box>
<Box
className="glass2"
w={"100%"}
pos={"fixed"}
top={0}
style={{
zIndex: 100,
overflow: "scroll"
}}
>
<NavbarMainMenu listNavbar={navbarListMenu} />
<Stack hiddenFrom="sm" bg={colors.grey[2]}>
<Group justify="space-between">
<ActionIcon variant="transparent" onClick={() => {
router.push("/darmasaba")
stateNav.mobileOpen = false
}}>
<IconHome />
</ActionIcon>
<Burger onClick={() => stateNav.mobileOpen = !stateNav.mobileOpen} color={colors["blue-button"]} opened={mobileOpen} />
</Group>
{mobileOpen && <motion.div
initial={{ x: 300 }}
animate={{ x: 0 }}
transition={{ duration: 0.1 }}
style={{
height: "100vh",
overflow: "scroll"
}}
>
<NavbarMobile listNavbar={navbarListMenu} />
</motion.div>}
</Stack>
</Box>
{(item || isSearch) && <Box className="glass" />}
</Box>
);
}
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
const router = useRouter()
return <Stack p={"md"} style={{ backgroundColor: "rgba(255, 255, 255, 0.3)" }}>
{listNavbar.map((item, k) => {
return <Stack key={k}>
<Group justify="space-between" onClick={() => {
router.push(item.href)
stateNav.mobileOpen = false
}}>
<Text c="dark.9"
style={{ fontWeight: "bold" }}
>{item.name}</Text>
<IconSquareArrowRight />
</Group>
{item.children && <NavbarMobile listNavbar={item.children} />}
</Stack>
})}
</Stack>
}

View File

@@ -6,9 +6,10 @@ import { ActionIcon, Button, Container, Flex, Image, Stack } from "@mantine/core
import { useHover } from "@mantine/hooks"
import { useTransitionRouter } from 'next-view-transitions'
import { useSnapshot } from "valtio"
import { MenuItem } from "../../types/menu-item"
import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu"
import { IconSearch } from "@tabler/icons-react"
export function NavbarMainMenu({ listNavbar }: {
listNavbar: MenuItem[]
@@ -25,7 +26,7 @@ export function NavbarMainMenu({ listNavbar }: {
md: "nowrap"
}}>
<ActionIcon radius={"100"} onClick={() => {
router.push("/")
router.push("/darmasaba")
stateNav.clear()
}} >
<Image radius={"100"} src={"/assets/images/darmasaba-icon.png"} alt="icon" w={24} h={24} loading="lazy" />
@@ -40,7 +41,7 @@ export function NavbarMainMenu({ listNavbar }: {
}}
>
{/* TODO: add icon search */}
{/* <MdSearch size={"1.5rem"} /> */}
<IconSearch size={"1.5rem"} />
</ActionIcon>
</Flex>
</Container>

View File

@@ -2,12 +2,13 @@
import stateNav from "@/state/state-nav";
import { Button, Container, Stack } from "@mantine/core";
import { motion } from "motion/react";
import Link from "next/link";
import { MenuItem } from "../../types/menu-item";
import _ from "lodash";
import { motion } from "motion/react";
import { MenuItem } from "../../../../types/menu-item";
import { useTransitionRouter } from 'next-view-transitions'
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
const router = useTransitionRouter()
return (
<motion.div
key={_.uniqueId()}
@@ -32,12 +33,15 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
item.map((item, k) => {
return (
<Button
key={k}
fz={"lg"}
color="dark.9"
variant="transparent"
component={Link}
href={item.href}
key={k}
onClick={() => {
router.push(item.href)
stateNav.item = null
stateNav.isSearch = false
}}
>
{item.name}
</Button>

View File

@@ -1,18 +1,13 @@
"use client";
import { Center, Container, Stack, Text } from "@mantine/core";
import { useRef } from "react";
import FlipOnScroll from "./FlipScroll";
function Content5() {
const ref = useRef(null);
return (
<Stack ref={ref}>
<Stack>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} h={720}>
<Center>
<Text fz={"3.4rem"}>CONTENT 5</Text>
<Text fz={"3.4rem"}>Berkolaborasi membangun desa</Text>
</Center>
<FlipOnScroll />
</Container>
</Stack>
);

View File

@@ -1,15 +1,15 @@
"use client";
import { Stack, Container, Center, Text } from "@mantine/core";
import Count from "./Count";
function Content6() {
return (
<Stack>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} h={720}>
<Center>
<Text fz={"3.4rem"}>CONTENT 6</Text>
<Text fz={"3.4rem"}>Indeks Kepuasan Masyarakat</Text>
</Center>
<Count />
</Container>
</Stack>
);

View File

@@ -0,0 +1,13 @@
import { Stack, Container, Center, Text } from "@mantine/core";
export default function Content7() {
return (
<Stack>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} h={720}>
<Center>
<Text fz={"3.4rem"}>APBDES Darmasaba</Text>
</Center>
</Container>
</Stack>
);
}

View File

@@ -0,0 +1,55 @@
import images from "@/con/images";
import { Center, Image, Paper, SimpleGrid } from "@mantine/core";
import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions';
const listImageModule = Object.values(images.module);
function ModuleItem({ item }: { item: string }) {
const router = useTransitionRouter();
return (
<Paper
onClick={() => {
router.push(`/module/c`);
}}
p={"md"}
bg={"white"}
radius={"32"}
pos={"relative"}
>
<Center h={"100%"}>
<motion.div
whileHover={{ scale: 1.05 }}
>
<Image src={item} alt="icon"
fit="contain"
sizes="100%"
loading="lazy"
style={{
objectFit: "contain",
objectPosition: "center"
}}
/>
</motion.div>
</Center>
</Paper>
);
}
function ModuleView() {
return (
<SimpleGrid
cols={{
base: 2,
md: 3,
}}
>
{listImageModule.map((item, k) => {
return <ModuleItem key={k} item={item} />;
})}
</SimpleGrid>
);
}
export default ModuleView;

View File

@@ -1,22 +1,21 @@
"use client";
import colors from "@/con/colors";
import {
BackgroundImage,
Flex,
Box,
Card,
Stack,
Image,
Text,
Button,
Title,
Card,
Flex,
Image,
Stack,
Text,
Title
} from "@mantine/core";
import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
function Content1() {
function LandingPage() {
return (
<BackgroundImage src={"/assets/images/bg-blur.png"}>
<Stack bg={colors["blue-button"]}>
<Flex
gap={"md"}
wrap={{
@@ -30,7 +29,6 @@ function Content1() {
base: "100%",
md: "60%",
}}
h={820}
py={{
base: "xs",
md: "72",
@@ -58,6 +56,7 @@ function Content1() {
>
<Flex gap={"md"} flex={1}>
<Box
pos={"relative"}
bg={"white"}
w={{
base: 64,
@@ -75,11 +74,11 @@ function Content1() {
<Image
src={"/assets/images/darmasaba-icon.png"}
alt="icon"
loading="lazy"
fit="contain"
sizes="100%"
/>
</Box>
<Box
pos={"relative"}
w={{
base: 64,
md: 72,
@@ -97,8 +96,8 @@ function Content1() {
<Image
src={"/assets/images/pudak-icon.png"}
alt="icon"
sizes={"100%"}
fit="contain"
loading="lazy"
/>
</Box>
</Flex>
@@ -112,23 +111,13 @@ function Content1() {
Pemerintah Desa
</Text>
<Title>DARMASABA</Title>
{/* <Text
fw={"bold"}
fz={{
base: "3rem",
md: "3rem",
sm: ""
}}
>
DARMASABA
</Text> */}
</Stack>
</Flex>
<ModuleView />
<SosmedView />
</Stack>
</Card>
<Stack
<Stack
align="center"
justify={"center"}
>
@@ -164,13 +153,8 @@ function Content1() {
<Image
src={"/assets/images/perbekel.png"}
alt="perbekel"
fit="contain" // Menyesuaikan gambar agar sesuai dengan kontainer
width={"100%"}
h={{
base: "500",
md: "100%",
}}
loading="lazy"
sizes="100%"
fit="contain"
/>
<Box
pos={"absolute"}
@@ -196,8 +180,8 @@ function Content1() {
</Box>
</Stack>
</Flex>
</BackgroundImage>
</Stack >
);
}
export default Content1;
export default LandingPage;

View File

@@ -9,9 +9,12 @@ import {
BackgroundImage,
Box,
Button,
Card,
Container,
Divider,
Group,
Image,
Paper,
Stack,
Text,
useMantineTheme,
@@ -19,6 +22,7 @@ import {
import { useMediaQuery } from "@mantine/hooks";
import Autoplay from "embla-carousel-autoplay";
import _ from "lodash";
import { useTransitionRouter } from "next-view-transitions";
import { useRef } from "react";
import useSWR from "swr";
@@ -39,6 +43,8 @@ function Content3() {
}
);
const router = useTransitionRouter()
return (
<Stack pos={"relative"} bg={colors.grey[1]} gap={"42"} py={"xl"}>
<Container w={{ base: "100%", md: "50%" }} p={"xl"}>
@@ -54,7 +60,9 @@ function Content3() {
{textHeading.des}
</Text>
<Box p={"md"}>
<Button variant="filled" bg={"dark"} radius={100}>
<Button onClick={() => {
router.push("/layanan")
}} variant="filled" bg={"dark"} radius={100}>
Lanjutkan
</Button>
</Box>
@@ -71,11 +79,17 @@ function Slider({ data }: { data: DataSlider[] }) {
const theme = useMantineTheme();
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
const autoplay = useRef(Autoplay({ delay: 2000 }));
const router = useTransitionRouter()
const slides = data.map((item) => (
<Carousel.Slide key={item.id}>
<BackgroundImage src={images["bg-slide3"]} h={height} p="xl" radius="md">
<Stack justify="space-between" h={"100%"} gap={0}>
<Carousel.Slide key={item.id} >
<Paper h={"100%"} pos={"relative"} style={{
backgroundImage: `url(${images["bg-slide3"]}) `,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}>
<Stack justify="space-between" h={"100%"} gap={0} p={"lg"} pos={"relative"} >
<Box p={"lg"}>
<Text
fw={"bold"}
@@ -89,12 +103,14 @@ function Slider({ data }: { data: DataSlider[] }) {
</Text>
</Box>
<Group justify="center">
<Button px={46} radius={"100"} size="md">
<Button onClick={() => {
router.push(`/layanan/${item.id}`)
}} px={46} radius={"100"} size="md">
Detail
</Button>
</Group>
</Stack>
</BackgroundImage>
</Paper>
</Carousel.Slide>
));

View File

@@ -0,0 +1,67 @@
'use client';
import { Stack, Box, Container, Button, Text } from "@mantine/core";
import { useTransitionRouter } from 'next-view-transitions'
function Penghargaan() {
const router = useTransitionRouter()
return (
<Stack pos={"relative"} h={720}>
<video
width="320"
height="240"
loop
autoPlay
muted
style={{
width: "100%",
height: "100%",
objectFit: "cover",
overflow: "hidden",
}}
>
<source src="/assets/videos/award.mp4" type="video/mp4" />
</video>
<Box
style={{
width: "100%",
height: "100%",
position: "absolute",
top: 0,
left: 0,
background: "rgba(0,0,0,0.6)",
}}
>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} h={720}>
<Stack justify="center" align="center">
<Text
style={{
textAlign: "center",
}}
fw={"bold"}
fz={"2.4rem"}
c={"white"}
>
Penghargaan
</Text>
<Stack align="center" gap={0}>
<Text fz={"1.4rem"} c={"white"}>
Juara 2 Lomba Video Pendek
</Text>
<Text fz={"1.4rem"} c={"white"}>
Juara 2 Duta Investasi
</Text>
<Text fz={"1.4rem"} c={"white"}>
Juara Favorit Lomba Video Pendek
</Text>
</Stack>
<Button onClick={() => router.push("/penghargaan")} variant="white" radius={100}>
Selanjutnya
</Button>
</Stack>
</Container>
</Box>
</Stack>
);
}
export default Penghargaan;

View File

@@ -0,0 +1,22 @@
import colors from "@/con/colors";
import { Box, Space, Stack } from "@mantine/core";
import Footer from "@/app/darmasaba/_com/Footer";
import { Navbar } from "@/app/darmasaba/_com/Navbar";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<Stack gap={0} bg={colors.grey[1]}>
<Navbar />
<Space h={{
base: "2.2rem",
md: "2.5rem"
}} />
<Box style={{
overflow: "scroll"
}}>
{children}
</Box>
<Footer />
</Stack>
)
}

View File

@@ -0,0 +1,24 @@
import LandingPage from "@/app/darmasaba/_com/main-page/landing-page";
import Penghargaan from "@/app/darmasaba/_com/main-page/penghargaan";
import Content3 from "@/app/darmasaba/_com/main-page/layanan";
import Content4 from "@/app/darmasaba/_com/main-page/content-4";
import Content5 from "@/app/darmasaba/_com/main-page/content-5";
import Content6 from "@/app/darmasaba/_com/main-page/content-6";
import colors from "@/con/colors";
// import ApiFetch from "@/lib/api-fetch";
import { Stack } from "@mantine/core";
import Content7 from "@/app/darmasaba/_com/main-page/content-7";
export default function Page() {
return (
<Stack bg={colors.grey[1]} gap={"4rem"}>
<LandingPage />
<Penghargaan />
<Content3 />
<Content4 />
<Content5 />
<Content6 />
<Content7 />
</Stack>
);
}

9
src/app/error.tsx Normal file
View File

@@ -0,0 +1,9 @@
'use client'
export default function Error() {
return (
<div>
<h1>Something went wrong!</h1>
<p>Sorry, something went wrong.</p>
</div>
);
}

View File

@@ -2,19 +2,22 @@
// All packages except `@mantine/hooks` require styles imports
import "@mantine/carousel/styles.css";
import "@mantine/core/styles.css";
import '@mantine/dropzone/styles.css';
import "animate.css";
import 'react-simple-toasts/dist/style.css';
import 'react-simple-toasts/dist/theme/dark.css';
import "./globals.css";
import LoadDataFirstClient from "@/com/LoadDataFirstClient";
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
import {
ColorSchemeScript,
MantineProvider,
createTheme,
mantineHtmlProps,
} from "@mantine/core";
import { MainLayout } from "../com/MainLayout";
import { ViewTransitions } from "next-view-transitions";
import { WebVitals } from "./_com/WebVitals";
export const metadata = {
title: "desa darmasaba",
@@ -47,10 +50,7 @@ export default function RootLayout({
</head>
<body>
<MantineProvider theme={theme}>
<MainLayout>
<WebVitals />
{children}
</MainLayout>
{children}
</MantineProvider>
</body>
<LoadDataFirstClient />

8
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,8 @@
export default function NotFound() {
return (
<div>
<h1>Not Found</h1>
<p>This is the not found page</p>
</div>
);
}

View File

@@ -1,32 +1,12 @@
import Content1 from "@/com/main-page/content-1";
import Content2 from "@/com/main-page/content-2";
import Content3 from "@/com/main-page/content-3";
import Content4 from "@/com/main-page/content-4";
import Content5 from "@/com/main-page/content-5";
import Content6 from "@/com/main-page/content-6";
import colors from "@/con/colors";
// import ApiFetch from "@/lib/api-fetch";
import { Stack } from "@mantine/core";
import SpashScreen from "./_com/SpashScreen";
export default function Page() {
return (
<Stack bg={colors.grey[1]} gap={"4rem"}>
<Content1 />
<Content2 />
<Content3 />
<Content4 />
<Content5 />
<Content6 />
<SpashScreen />
</Stack>
);
}
// async function Content3Loader() {
// const { data } = await fetch("/api/layanan").then((v) => v.json());
// return <Content3 data={data} />;
// }
// async function Content4Loader() {
// const { data } = await ApiFetch.api.potensi.get();
// return <Content4 data={data?.data as any} />;
// }

View File

@@ -1,32 +0,0 @@
"use client";
import colors from "@/con/colors";
import navbarListMenu from "@/con/navbar-list-menu";
import stateNav from "@/state/state-nav";
import { Box, Burger, Group } from "@mantine/core";
import { useSnapshot } from "valtio";
import { NavbarMainMenu } from "./NavbarMainMenu";
export function Navbar() {
const { item, isSearch } = useSnapshot(stateNav);
return (
<Box>
<Box
className="glass2"
w={"100%"}
pos={"fixed"}
top={0}
style={{
zIndex: 100,
}}
>
<NavbarMainMenu listNavbar={navbarListMenu} />
<Group justify="end">
<Box hiddenFrom="sm">
<Burger color={colors["blue-button"]} />
</Box>
</Group>
</Box>
{(item || isSearch) && <Box className="glass" />}
</Box>
);
}

View File

@@ -1,49 +0,0 @@
import images from "@/con/images";
import stateNav from "@/state/state-nav";
import { Card, Image, SimpleGrid, Stack } from "@mantine/core";
import { useHover } from "@mantine/hooks";
import { useTransitionRouter } from 'next-view-transitions'
const listImageModule = Object.values(images.module);
function ModuleItem({ item }: { item: string }) {
const router = useTransitionRouter();
const { ref, hovered } = useHover();
return (
<Card
onClick={() => {
stateNav.module = item;
router.push("/module");
}}
ref={ref}
p={"md"}
bg={"white"}
radius={"32"}
style={{
border: `2px solid ${hovered ? "lightgray" : "transparent"}`,
}}
>
<Stack justify="end" h={"100%"}>
<Image src={item} alt="icon" fit="contain" loading="lazy" />
</Stack>
</Card>
);
}
function ModuleView() {
return (
<SimpleGrid
cols={{
base: 3,
md: 4,
}}
>
{listImageModule.map((item, k) => {
return <ModuleItem key={k} item={item} />;
})}
</SimpleGrid>
);
}
export default ModuleView;

View File

@@ -1,65 +0,0 @@
'use client';
import { Stack, Box, Container, Button, Text } from "@mantine/core";
function Content2() {
return (
<Stack pos={"relative"} h={720}>
<video
width="320"
height="240"
loop
autoPlay
muted
style={{
width: "100%",
height: "100%",
objectFit: "cover",
overflow: "hidden",
}}
>
<source src="/assets/videos/award.mp4" type="video/mp4" />
</video>
<Box
style={{
width: "100%",
height: "100%",
position: "absolute",
top: 0,
left: 0,
background: "rgba(0,0,0,0.6)",
}}
>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} h={720}>
<Stack justify="center" align="center">
<Text
style={{
textAlign: "center",
}}
fw={"bold"}
fz={"2.4rem"}
c={"white"}
>
Penghargaan
</Text>
<Stack align="center" gap={0}>
<Text fz={"1.4rem"} c={"white"}>
Juara 2 Lomba Video Pendek
</Text>
<Text fz={"1.4rem"} c={"white"}>
Juara 2 Duta Investasi
</Text>
<Text fz={"1.4rem"} c={"white"}>
Juara Favorit Lomba Video Pendek
</Text>
</Stack>
<Button variant="white" radius={100}>
Selanjutnya
</Button>
</Stack>
</Container>
</Box>
</Stack>
);
}
export default Content2;

View File

@@ -1,30 +0,0 @@
"use client";
import { Stack } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { animate, motion, useMotionValue, useTransform } from "motion/react";
export default function Count() {
const count = useMotionValue(0);
const rounded = useTransform(() => Math.round(count.get()));
useShallowEffect(() => {
const controls = animate(count, 100, { duration: 5 });
return () => controls.stop();
}, []);
return (
<Stack>
<motion.pre style={text}>{rounded}</motion.pre>
</Stack>
);
}
/**
* ============== Styles ================
*/
const text = {
fontSize: 64,
color: "#4ff0b7",
};

View File

@@ -12,7 +12,8 @@ const colors = {
}
},
"grey": {
"1": "#F4F5F6"
"1": "#F4F5F6",
"2": "#CBCACD"
}
}

View File

@@ -2,37 +2,37 @@ const navbarListMenu = [
{
id: "1",
name: "Desa",
href: "/desa",
href: "/darmasaba/desa",
children: [
{
id: "1.1",
name: "profile",
href: "/desa/profile"
href: "/darmasaba/desa/profile"
},
{
id: "1.2",
name: "potensi",
href: "/desa/potensi"
href: "/darmasaba/desa/potensi"
},
{
id: "1.3",
name: "berita",
href: "/desa/berita"
href: "/darmasaba/desa/berita"
},
{
id: "1.4",
name: "pengumuman",
href: "/desa/pengumuman"
href: "/darmasaba/desa/pengumuman"
},
{
id: "1.5",
name: "galery",
href: "/desa/galery"
href: "/darmasaba/desa/galery"
},
{
id: "1.6",
name: "layanan",
href: "/desa/layanan"
href: "/darmasaba/desa/layanan"
},
]
@@ -40,240 +40,240 @@ const navbarListMenu = [
{
id: "2",
name: "Kesehatan",
href: "/kesehatan",
href: "/darmasaba/kesehatan",
children: [
{
id: "2.1",
name: "Posyandu",
href: "/kesehatan/posyandu"
href: "/darmasaba/kesehatan/posyandu"
},
{
id: "2.2",
name: "Data Kesehatan Warga",
href: "/kesehatan/data-kesehatan-warga"
href: "/darmasaba/kesehatan/data-kesehatan-warga"
},
{
id: "2.3",
name: "Puskesmas",
href: "/kesehatan/puskesmas"
href: "/darmasaba/kesehatan/puskesmas"
},
{
id: "2.4",
name: "Program Kesehatan",
href: "/kesehatan/program-kesehatan"
href: "/darmasaba/kesehatan/program-kesehatan"
},
{
id: "2.5",
name: "Penanganan Darurat",
href: "/kesehatan/penanganan-darurat"
href: "/darmasaba/kesehatan/penanganan-darurat"
},
{
id: "2.6",
name: "Kontak Darurat",
href: "/kesehatan/kontak-darurat"
href: "/darmasaba/kesehatan/kontak-darurat"
},
{
id: "2.7",
name: "Info Wabah/Penyakit",
href: "/kesehatan/info-wabah-penyakit"
href: "/darmasaba/kesehatan/info-wabah-penyakit"
}
]
},
{
id: "3",
name: "Keamanan",
href: "/keamanan",
href: "/darmasaba/keamanan",
children: [
{
id: "3.1",
name: "Keamanan Lingkungan (Pecalang/Patwal)",
href: "/keamanan/keamanan-lingkungan"
href: "/darmasaba/keamanan/keamanan-lingkungan"
},
{
id: "3.2",
name: "Polsek Terdekat",
href: "/keamanan/polsek-terdekat"
href: "/darmasaba/keamanan/polsek-terdekat"
},
{
id: "3.3",
name: "Kontak Darurat",
href: "/keamanan/kontak-darurat"
href: "/darmasaba/keamanan/kontak-darurat"
},
{
id: "3.4",
name: "Pencegahan Kriminalitas",
href: "/keamanan/pencegahan-kriminalitas"
href: "/darmasaba/keamanan/pencegahan-kriminalitas"
},
{
id: "3.5",
name: "Laporan Publik",
href: "/keamanan/laporan-publik"
href: "/darmasaba/keamanan/laporan-publik"
},
{
id: "3.6",
name: "Tips Keamanan",
href: "/keamanan/tips-keamanan"
href: "/darmasaba/keamanan/tips-keamanan"
}
]
},
{
id: "4",
name: "Ekonomi",
href: "/ekonomi",
href: "/darmasaba/ekonomi",
children: [
{
id: "4.1",
name: "Pasar Desa",
href: "/ekonomi/pasar-desa"
href: "/darmasaba/ekonomi/pasar-desa"
},
{
id: "4.2",
name: "Koperasi",
href: "/ekonomi/koperasi"
href: "/darmasaba/ekonomi/koperasi"
},
{
id: "4.3",
name: "UMKM",
href: "/ekonomi/umkm"
href: "/darmasaba/ekonomi/umkm"
},
{
id: "4.4",
name: "Data Ekonomi Desa",
href: "/ekonomi/data-ekonomi-desa"
href: "/darmasaba/ekonomi/data-ekonomi-desa"
},
{
id: "4.5",
name: "Pelatihan Wirausaha",
href: "/ekonomi/pelatihan-wirausaha"
href: "/darmasaba/ekonomi/pelatihan-wirausaha"
},
{
id: "4.6",
name: "Bantuan & Pendanaan",
href: "/ekonomi/bantuan-pendanaan"
href: "/darmasaba/ekonomi/bantuan-pendanaan"
},
{
id: "4.7",
name: "Investasi Desa",
href: "/ekonomi/investasi-desa"
href: "/darmasaba/ekonomi/investasi-desa"
},
{
id: "4.8",
name: "Produk Unggulan",
href: "/ekonomi/produk-unggulan"
href: "/darmasaba/ekonomi/produk-unggulan"
},
{
id: "4.9",
name: "Lowongan Kerja Lokal",
href: "/ekonomi/lowongan-kerja-lokal"
href: "/darmasaba/ekonomi/lowongan-kerja-lokal"
}
]
}, {
id: "5",
name: "Inovasi",
href: "/inovasi",
href: "/darmasaba/inovasi",
children: [
{
id: "5.1",
name: "Desa Digital/Smart Village",
href: "/inovasi/desa-digital-smart-village"
href: "/darmasaba/inovasi/desa-digital-smart-village"
},
{
id: "5.2",
name: "Layanan Online Desa",
href: "/inovasi/layanan-online-desa"
href: "/darmasaba/inovasi/layanan-online-desa"
},
{
id: "5.3",
name: "Program Kreatif Desa",
href: "/inovasi/program-kreatif-desa"
href: "/darmasaba/inovasi/program-kreatif-desa"
},
{
id: "5.4",
name: "Kolaborasi Inovasi",
href: "/inovasi/kolaborasi-inovasi"
href: "/darmasaba/inovasi/kolaborasi-inovasi"
},
{
id: "5.5",
name: "Info Teknologi Tepat Guna",
href: "/inovasi/info-teknologi-tepat-guna"
href: "/darmasaba/inovasi/info-teknologi-tepat-guna"
},
{
id: "5.6",
name: "Ajukan Ide Inovatif",
href: "/inovasi/ajukan-ide-inovatif"
href: "/darmasaba/inovasi/ajukan-ide-inovatif"
}
]
}, {
id: "6",
name: "Lingkungan",
href: "/lingkungan",
href: "/darmasaba/lingkungan",
children: [
{
id: "6.1",
name: "Pengelolaan Sampah (Bank Sampah)",
href: "/lingkungan/pengelolaan-sampah-bank-sampah"
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah"
},
{
id: "6.2",
name: "Program Penghijauan",
href: "/lingkungan/program-penghijauan"
href: "/darmasaba/lingkungan/program-penghijauan"
},
{
id: "6.3",
name: "Data Lingkungan Desa",
href: "/lingkungan/data-lingkungan-desa"
href: "/darmasaba/lingkungan/data-lingkungan-desa"
},
{
id: "6.4",
name: "Gotong Royong",
href: "/lingkungan/gotong-royong"
href: "/darmasaba/lingkungan/gotong-royong"
},
{
id: "6.5",
name: "Edukasi Lingkungan",
href: "/lingkungan/edukasi-lingkungan"
href: "/darmasaba/lingkungan/edukasi-lingkungan"
},
{
id: "6.6",
name: "Konservasi Adat Bali",
href: "/lingkungan/konservasi-adat-bali"
href: "/darmasaba/lingkungan/konservasi-adat-bali"
}
]
}, {
id: "7",
name: "Pendidikan",
href: "/pendidikan",
href: "/darmasaba/pendidikan",
children: [
{
id: "7.1",
name: "Info Sekolah & PAUD",
href: "/pendidikan/info-sekolah-paud"
href: "/darmasaba/pendidikan/info-sekolah-paud"
},
{
id: "7.2",
name: "Beasiswa Desa",
href: "/pendidikan/beasiswa-desa"
href: "/darmasaba/pendidikan/beasiswa-desa"
},
{
id: "7.3",
name: "Program Pendidikan Anak",
href: "/pendidikan/program-pendidikan-anak"
href: "/darmasaba/pendidikan/program-pendidikan-anak"
},
{
id: "7.4",
name: "Bimbingan Belajar Desa",
href: "/pendidikan/bimbingan-belajar-desa"
href: "/darmasaba/pendidikan/bimbingan-belajar-desa"
},
{
id: "7.5",
name: "Pendidikan Non Formal",
href: "/pendidikan/pendidikan-non-formal"
href: "/darmasaba/pendidikan/pendidikan-non-formal"
},
{
id: "7.6",
name: "Perpustakaan Desa",
href: "/pendidikan/perpustakaan-desa"
href: "/darmasaba/pendidikan/perpustakaan-desa"
}
]
}

40
src/con/router.ts Normal file
View File

@@ -0,0 +1,40 @@
const pages = {
"home": "/",
"darmasaba": {
"home": "/daramasaba",
"desa": "/darmasaba/desa",
"ekonomi": "/darmasaba/ekonomi",
"keamanan": "/darmasaba/keamanan",
"kesehatan": "/darmasaba/kesehatan",
"inovasi": "/darmasaba/inovasi",
"lingkungan": "/darmasaba/lingkungan",
"pendidikan": "/darmasaba/pendidikan",
"module": {
"daves": "/darmasaba/module/daves",
"mangan": "/darmasaba/module/mangan",
"bicara-darma": "/darmasaba/module/bicara-darma",
"bares": "/darmasaba/module/bares",
"sajjana-dharma-raksaka": "/darmasaba/module/sajjana-dharma-raksaka",
"pdkt": "/darmasaba/module/pdkt",
"gelah-melah": "/darmasaba/module/gelah-melah",
"inovasi-desa-darmasaba": "/darmasaba/module/inovasi-desa-darmasaba"
}
},
}
const apies = {
"/api": "home",
"/api/about": "about",
"/api/contact": "contact",
}
const router = {
pages,
apies
} as const
export default router

53
src/lib/EnvStringParse.ts Normal file
View File

@@ -0,0 +1,53 @@
type EnvVariable = { key: string; value: string };
class EnvStringParser {
/**
* Parses an environment string into a key-value object.
* @param envString - The environment string to parse.
* @param env - Optional custom environment variables (defaults to `process.env` in Node.js).
* @returns A Record<string, string> containing parsed environment variables.
*/
static parse(envString: string, env: Record<string, string | undefined> = process.env): Record<string, string> {
const envVars: EnvVariable[] = [];
// Split the string into lines
const lines = envString.split(/\r?\n/); // Handle both \n and \r\n line endings
for (const line of lines) {
const trimmedLine = line.trim();
// Skip comments and empty lines
if (!trimmedLine || trimmedLine.startsWith("#")) continue;
// Match key-value pairs with support for quoted values
const match = trimmedLine.match(/^([\w.-]+)=(?:"([^"]*)"|'([^']*)'|([^#\s]*))/);
if (!match) {
console.warn(`Skipping invalid line: ${trimmedLine}`);
continue;
}
const key = match[1];
let value = match[2] || match[3] || match[4] || ""; // Handle double quotes, single quotes, or unquoted values
// Resolve environment variable placeholders like ${VAR_NAME}
value = value.replace(/\$\{(\w+)\}/g, (_, varName) => {
if (env[varName]) {
return env[varName]!;
} else {
console.warn(`Environment variable ${varName} is not defined`);
return "";
}
});
envVars.push({ key, value });
}
// Convert array of EnvVariable objects into a Record<string, string>
const envObj: Record<string, string> = {};
for (const { key, value } of envVars) {
envObj[key] = value;
}
return envObj;
}
}
export default EnvStringParser;

View File

@@ -1,6 +1,5 @@
import { AppServer } from '@/app/api/[[...slugs]]/route'
import { treaty } from '@elysiajs/eden'
const ApiFetch = treaty<AppServer>(process.env.NEXT_PUBLIC_HOST || 'localhost:3000')
const ApiFetch = treaty<AppServer>(process.env.NEXT_PUBLIC_WIBU_URL || 'localhost:3000')
export default ApiFetch

67
src/schema.ts Normal file
View File

@@ -0,0 +1,67 @@
import { SchemaObject } from '@paljs/types'
export const schema: SchemaObject = {
models: [
{
name: 'Layanan',
fields: [
{
name: 'id',
type: 'String',
isId: true,
unique: false,
defaultValue: 'cuid()',
list: false,
required: true,
kind: 'scalar',
documentation: '',
relationField: false,
},
{
name: 'name',
type: 'String',
isId: false,
unique: true,
list: false,
required: true,
kind: 'scalar',
documentation: '',
relationField: false,
},
],
documentation: '',
},
{
name: 'Potensi',
fields: [
{
name: 'id',
type: 'String',
isId: true,
unique: false,
defaultValue: 'cuid()',
list: false,
required: true,
kind: 'scalar',
documentation: '',
relationField: false,
},
{
name: 'name',
type: 'String',
isId: false,
unique: true,
list: false,
required: true,
kind: 'scalar',
documentation: '',
relationField: false,
},
],
documentation: '',
},
],
enums: [],
dataSource: { provider: 'postgresql', url: 'env("DATABASE_URL")' },
generators: [{ name: 'client', provider: 'prisma-client-js' }],
}

View File

@@ -0,0 +1,38 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
const stateListImage = proxy<{
list: { name: string; url: string; total: number }[] | null;
page: number;
count: number;
total: number | undefined;
load: (params?: { search?: string }) => Promise<void>;
del: ({ name }: { name: string }) => Promise<void>;
}>({
list: null,
page: 1,
count: 20,
total: undefined,
async load(params?: { search?: string }) {
const { search = "" } = params ?? {};
const { data } = await ApiFetch.api.imgs.get({
query: {
page: this.page,
count: this.count,
search,
},
});
this.list = data;
if (data?.[0]?.total) {
this.total = Math.ceil(data[0].total / this.count);
} else {
this.total = undefined;
}
},
async del({ name }: { name: string }) {
await ApiFetch.api.img({ name }).delete();
this.load();
},
});
export default stateListImage;

View File

@@ -6,7 +6,8 @@ const stateNav = proxy<{
item: MenuItem[] | null
isSearch: boolean,
clear: () => void,
module: string | null
module: string | null,
mobileOpen: boolean
}>({
hover: false,
item: null,
@@ -16,7 +17,8 @@ const stateNav = proxy<{
stateNav.item = null
stateNav.isSearch = false
},
module: null
module: null,
mobileOpen: false
})
export default stateNav

9
types/bun.ts Normal file
View File

@@ -0,0 +1,9 @@
declare module "bun" {
interface Env {
PORT: string;
NEXT_PUBLIC_WIBU_URL: string;
DATABASE_URL: string;
WIBU_UPLOAD_DIR: string;
WIBU_CACHE_DIR: string;
}
}

View File

@@ -1,11 +1,68 @@
import path from 'path';
import fs from 'fs';
import { compress } from 'compress-pdf';
import { spawn } from 'bun'
(async () => {
const pdf = path.resolve("/Users/bip/Downloads", 'komoditas.pdf');
const buffer = await compress(pdf);
async function proc(params?: {
env?: Record<string, string | undefined>
cmd?: string
cwd?: string
timeout?: number
onStdOut?: (chunk: string) => void
onStdErr?: (chunk: string) => void
}) {
const { env = {}, cmd, cwd = "./", timeout = 30000 } = params || {}
return new Promise(async (resolve, reject) => {
const std = {
stdout: "",
stderr: "",
}
const compressedPdf = path.resolve(__dirname, 'compressed_pdf.pdf');
await fs.promises.writeFile(compressedPdf, buffer);
})();
if (!cmd) {
reject(new Error("cmd is required"))
return
}
const decoder = new TextDecoder()
const child = spawn(["/bin/bash", "-c", cmd], {
cwd,
env: {
PATH: process.env.PATH,
...env
},
})
const timeOut = setTimeout(() => {
child.kill()
clearTimeout(timeOut)
reject("timeout")
}, timeout)
const resOut = new Response(child.stdout)
const resErr = new Response(child.stderr)
if (resOut && resOut.body) {
for await (const chunk of resOut.body as unknown as AsyncIterable<Uint8Array>) {
const text = decoder.decode(chunk)
std.stdout += text
if (params?.onStdOut) {
params.onStdOut(text)
}
}
}
if (resErr && resErr.body) {
for await (const chunk of resErr.body as unknown as AsyncIterable<Uint8Array>) {
const text = decoder.decode(chunk)
std.stderr += text
params?.onStdErr?.(text)
}
}
clearTimeout(timeOut)
if (!child.killed) {
child.kill()
}
resolve(std)
})
}
export default proc

104
xcoba2.ts Normal file
View File

@@ -0,0 +1,104 @@
import { spawn } from 'bun';
async function proc(params?: {
env?: Record<string, string | undefined>;
cmd?: string;
cwd?: string;
timeout?: number;
exitCode?: number;
onStdOut?: (chunk: string) => void;
onStdErr?: (chunk: string) => void;
onStdio?: (chunk: string) => void;
}) {
const { env = {}, cmd, cwd = "./", timeout = 600000 } = params || {};
return new Promise(async (resolve, reject) => {
if (!cmd || typeof cmd !== "string") {
return reject(new Error("Invalid or missing command"));
}
const std = {
stdout: "",
stderr: "",
stdio: "",
};
try {
// Spawn the child process
const child = spawn(cmd.split(" "), {
cwd,
env: {
PATH: process.env.PATH,
...env,
},
});
// Set a timeout to kill the process if it takes too long
const timeOut = setTimeout(() => {
try {
child.kill();
} catch (err) {
console.warn("Failed to kill child process:", err);
}
reject(new Error("Process timed out"));
}, timeout);
// Read stdout and stderr as text
const [stdout, stderr] = await Promise.all([
readStream(child.stdout),
child.stderr ? readStream(child.stderr) : undefined,
]);
// Handle stdout
std.stdout = stdout;
std.stdio += stdout;
if (params?.onStdOut) {
params.onStdOut(stdout.trim());
}
if (params?.onStdio) {
params.onStdio(stdout.trim());
}
// Handle stderr
std.stderr = stderr ?? "";
std.stdio += stderr;
if (params?.onStdErr) {
params.onStdErr((stderr ?? "").trim());
}
if (params?.onStdio) {
params.onStdio((stderr ?? "").trim());
}
clearTimeout(timeOut);
resolve(std);
} catch (err) {
reject(err);
}
});
}
async function readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let result = '';
let done = false;
while (!done) {
const { value, done: streamDone } = await reader.read();
done = streamDone;
if (value) {
result += decoder.decode(value, { stream: true });
}
}
result += decoder.decode(); // flush any remaining data
return result.trim();
}
export default proc;
proc({
cmd: "bun run build",
cwd: "./",
onStdio: (text) => {
console.log(text.trim());
}
})

3
xx.ts Normal file
View File

@@ -0,0 +1,3 @@
import path from "path";
console.log(path.basename("/apa/kanar.png", ".png"))