diff --git a/.gemini/hooks/telegram-notify.ts b/.gemini/hooks/telegram-notify.ts
new file mode 100755
index 00000000..063d337e
--- /dev/null
+++ b/.gemini/hooks/telegram-notify.ts
@@ -0,0 +1,65 @@
+#!/usr/bin/env bun
+import { readFileSync } from "node:fs";
+
+// Fungsi untuk mencari string terpanjang dalam objek (biasanya balasan AI)
+function findLongestString(obj: any): string {
+ let longest = "";
+ const search = (item: any) => {
+ if (typeof item === "string") {
+ if (item.length > longest.length) longest = item;
+ } else if (Array.isArray(item)) {
+ item.forEach(search);
+ } else if (item && typeof item === "object") {
+ Object.values(item).forEach(search);
+ }
+ };
+ search(obj);
+ return longest;
+}
+
+async function run() {
+ try {
+ const inputRaw = readFileSync(0, "utf-8");
+ if (!inputRaw) return;
+ const input = JSON.parse(inputRaw);
+
+ // DEBUG: Lihat struktur asli di console terminal (stderr)
+ console.error("DEBUG KEYS:", Object.keys(input));
+
+ const BOT_TOKEN = process.env.BOT_TOKEN;
+ const CHAT_ID = process.env.CHAT_ID;
+
+ const sessionId = input.session_id || "unknown";
+
+ // Cari teks secara otomatis di seluruh objek JSON
+ let finalText = findLongestString(input.response || input);
+
+ if (!finalText || finalText.length < 5) {
+ finalText =
+ "Teks masih gagal diekstraksi. Struktur: " +
+ Object.keys(input).join(", ");
+ }
+
+ const message =
+ `✅ *Gemini Task Selesai*\n\n` +
+ `🆔 Session: \`${sessionId}\` \n\n` +
+ `🧠 Output:\n${finalText.substring(0, 3500)}`;
+
+ await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ chat_id: CHAT_ID,
+ text: message,
+ parse_mode: "Markdown",
+ }),
+ });
+
+ process.stdout.write(JSON.stringify({ status: "continue" }));
+ } catch (err) {
+ console.error("Hook Error:", err);
+ process.stdout.write(JSON.stringify({ status: "continue" }));
+ }
+}
+
+run();
diff --git a/.gemini/settings.json b/.gemini/settings.json
new file mode 100644
index 00000000..ed736356
--- /dev/null
+++ b/.gemini/settings.json
@@ -0,0 +1,17 @@
+{
+ "hooks": {
+ "AfterAgent": [
+ {
+ "matcher": "*",
+ "hooks": [
+ {
+ "name": "telegram-notify",
+ "type": "command",
+ "command": "bun $GEMINI_PROJECT_DIR/.gemini/hooks/telegram-notify.ts",
+ "timeout": 10000
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 00000000..4c8370da
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,62 @@
+# Project: Desa Darmasaba
+
+## Project Overview
+
+The `desa-darmasaba` project is a Next.js (version 15+) application developed with TypeScript. It serves as an official platform for Desa Darmasaba (a village in Badung, Bali), offering various public services, news, and detailed village profiles.
+
+**Key Technologies:**
+
+* **Frontend Framework:** Next.js (v15+) with React (v19+)
+* **Language:** TypeScript
+* **UI Library:** Mantine UI
+* **Database ORM:** Prisma (v6+)
+* **Database:** PostgreSQL (as configured in `prisma/schema.prisma`)
+* **API Framework:** Elysia (used for API routes, as seen in dependencies)
+* **State Management:** Potentially Jotai and Valtio (listed in dependencies)
+* **Image Processing:** Sharp
+* **Package Manager:** Likely Bun, given `bun.lockb` and the `prisma:seed` script.
+
+The application architecture follows the Next.js App Router structure, with comprehensive data models defined in `prisma/schema.prisma` covering various domains like public information, health, security, economy, innovation, environment, and education. It also includes configurations for image handling and caching.
+
+## Building and Running
+
+This project uses `bun` as the package manager. Ensure Bun is installed to run these commands.
+
+* **Install Dependencies:**
+ ```bash
+ bun install
+ ```
+
+* **Development Server:**
+ Runs the Next.js development server.
+ ```bash
+ bun run dev
+ ```
+
+* **Build for Production:**
+ Builds the Next.js application for production deployment.
+ ```bash
+ bun run build
+ ```
+
+* **Start Production Server:**
+ Starts the Next.js application in production mode.
+ ```bash
+ bun run start
+ ```
+
+* **Database Seeding:**
+ Executes the Prisma seeding script to populate the database.
+ ```bash
+ bun run prisma:seed
+ ```
+
+## Development Conventions
+
+* **Coding Language:** TypeScript is strictly enforced.
+* **Frontend Framework:** Next.js App Router for page and component structuring.
+* **UI/UX:** Adherence to Mantine UI component library for consistent styling and user experience.
+* **Database Interaction:** Prisma ORM is used for all database operations, with a PostgreSQL database.
+* **Linting:** ESLint is configured with `next/core-web-vitals` and `next/typescript` to maintain code quality and adherence to Next.js and TypeScript best practices.
+* **Styling:** PostCSS is used, with `postcss-preset-mantine` and `postcss-simple-vars` defining Mantine-specific breakpoints and other CSS variables.
+* **Imports:** Absolute imports are configured using `@/*` which resolves to the `src/` directory.
diff --git a/QWEN.md b/QWEN.md
index e69de29b..21d07dbf 100644
--- a/QWEN.md
+++ b/QWEN.md
@@ -0,0 +1,232 @@
+# Desa Darmasaba - Village Management System
+
+## Project Overview
+
+Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
+
+### Key Technologies
+- **Framework**: Next.js 15 with App Router
+- **Language**: TypeScript with strict mode
+- **Styling**: Mantine UI components with custom CSS
+- **Backend**: Elysia.js API server integrated with Next.js
+- **Database**: PostgreSQL with Prisma ORM
+- **State Management**: Valtio for global state
+- **Authentication**: JWT with iron-session
+
+### Architecture
+The application follows a modular architecture with:
+- A main frontend built with Next.js and Mantine UI
+- An integrated Elysia.js API server for backend operations
+- Prisma ORM for database interactions
+- File storage integration with Seafile
+- Multiple domain-specific modules (PPID, health, security, education, etc.)
+
+## Building and Running
+
+### Prerequisites
+- Node.js (with Bun runtime)
+- PostgreSQL database
+- Seafile server for file storage
+
+### Setup Instructions
+1. Install dependencies:
+ ```bash
+ bun install
+ ```
+
+2. Set up environment variables in `.env.local`:
+ ```
+ DATABASE_URL=your_postgresql_connection_string
+ SEAFILE_TOKEN=your_seafile_token
+ SEAFILE_REPO_ID=your_seafile_repo_id
+ SEAFILE_BASE_URL=your_seafile_base_url
+ SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
+ SEAFILE_URL=your_seafile_api_url
+ WIBU_UPLOAD_DIR=your_upload_directory
+ ```
+
+3. Generate Prisma client:
+ ```bash
+ bunx prisma generate
+ ```
+
+4. Push database schema:
+ ```bash
+ bunx prisma db push
+ ```
+
+5. Seed the database:
+ ```bash
+ bun run prisma/seed.ts
+ ```
+
+6. Run the development server:
+ ```bash
+ bun run dev
+ ```
+
+### Available Scripts
+- `bun run dev` - Start development server
+- `bun run build` - Build for production
+- `bun run start` - Start production server
+- `bun run prisma/seed.ts` - Run database seeding
+- `bunx prisma generate` - Generate Prisma client
+- `bunx prisma db push` - Push schema changes to database
+- `bunx prisma studio` - Open Prisma Studio GUI
+
+## Development Conventions
+
+### Code Structure
+```
+src/
+├── app/ # Next.js app router pages
+│ ├── admin/ # Admin dashboard pages
+│ ├── api/ # API routes with Elysia.js
+│ ├── darmasaba/ # Public-facing village pages
+│ └── ...
+├── con/ # Constants and configuration
+├── hooks/ # React hooks
+├── lib/ # Utility functions and configurations
+├── middlewares/ # Next.js middleware
+├── state/ # Global state management
+├── store/ # Additional state management
+├── types/ # TypeScript type definitions
+└── utils/ # Utility functions
+```
+
+### Import Conventions
+- Use absolute imports with `@/` alias (configured in tsconfig.json)
+- Group imports: external libraries first, then internal modules
+- Keep import statements organized and remove unused imports
+
+```typescript
+// External libraries
+import { useState } from 'react'
+import { Button, Stack } from '@mantine/core'
+
+// Internal modules
+import ApiFetch from '@/lib/api-fetch'
+import { MyComponent } from '@/components/my-component'
+```
+
+### TypeScript Configuration
+- Strict mode enabled (`"strict": true`)
+- Target: ES2017
+- Module resolution: bundler
+- Path alias: `@/*` maps to `./src/*`
+
+### Naming Conventions
+- **Components**: PascalCase (e.g., `UploadImage.tsx`)
+- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
+- **Variables/Functions**: camelCase
+- **Constants**: UPPER_SNAKE_CASE
+- **Database Models**: PascalCase (Prisma convention)
+
+### Error Handling
+- Use try-catch blocks for async operations
+- Implement proper error boundaries in React components
+- Log errors appropriately without exposing sensitive data
+- Use Zod for runtime validation and type safety
+
+### API Structure
+- Backend uses Elysia.js with TypeScript
+- API routes are in `src/app/api/[[...slugs]]/` directory
+- Use treaty client for type-safe API calls
+- Follow RESTful conventions for endpoints
+- Include proper HTTP status codes and error responses
+
+### Database Operations
+- Use Prisma client from `@/lib/prisma.ts`
+- Database connection includes graceful shutdown handling
+- Use transactions for complex operations
+- Implement proper error handling for database queries
+
+### Component Guidelines
+- Use functional components with hooks
+- Implement proper prop types with TypeScript interfaces
+- Use Mantine components for UI consistency
+- Follow atomic design principles when possible
+- Add loading states and error states for async operations
+
+### State Management
+- Use Valtio proxies for global state
+- Keep local state in components when possible
+- Use SWR for server state caching
+- Implement optimistic updates for better UX
+
+### Styling
+- Primary: Mantine UI components
+- Use Mantine theme system for customization
+- Custom CSS should be minimal and scoped
+- Follow responsive design principles
+- Use semantic HTML5 elements
+
+### Security Practices
+- Validate all user inputs with Zod schemas
+- Use JWT tokens for authentication
+- Implement proper CORS configuration
+- Never expose database credentials or API keys
+- Use HTTPS in production
+- Implement rate limiting for sensitive endpoints
+
+### Performance Considerations
+- Use Next.js Image optimization
+- Implement proper caching strategies
+- Use React.memo for expensive components
+- Optimize bundle size with dynamic imports
+- Use Prisma query optimization
+
+## Domain Modules
+
+The application is organized into several domain modules:
+
+1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
+2. **Health**: Health facilities, programs, emergency response, disease information
+3. **Security**: Community security, emergency contacts, crime prevention
+4. **Education**: Schools, scholarships, educational programs
+5. **Economy**: Local markets, BUMDes, employment data
+6. **Environment**: Environmental data, conservation, waste management
+7. **Innovation**: Digital services, innovation programs
+8. **Culture**: Village traditions, music, cultural preservation
+
+Each module has its own section in both the admin panel and public-facing areas.
+
+## File Storage Integration
+
+The application integrates with Seafile for file storage, with specific handling for:
+- Images and documents
+- Public sharing capabilities
+- CDN URL generation
+- Batch processing of assets
+
+## Testing
+
+Currently no formal test framework is configured. When adding tests:
+- Consider Jest or Vitest for unit testing
+- Use Playwright for E2E testing
+- Update this section with specific test commands
+
+## Deployment
+
+The application includes deployment scripts in the `NOTE.md` file that outline:
+- Automated deployment with GitHub API integration
+- Environment-specific configurations
+- PM2 process management
+- Release management with versioning
+
+## Troubleshooting
+
+Common issues and solutions:
+- **API endpoints returning 404**: Check that environment variables are properly configured
+- **Database connection errors**: Verify DATABASE_URL in environment variables
+- **File upload issues**: Ensure Seafile integration is properly configured
+- **Build failures**: Run `bunx prisma generate` before building
+
+## Development Workflow
+
+1. Always run type checking before committing: `bunx tsc --noEmit`
+2. Run linting to catch style issues: `bun run eslint .`
+3. Test database changes with `bunx prisma db push`
+4. Use the integrated Swagger docs at `/api/docs` for API testing
+5. Check environment variables are properly configured
+6. Verify responsive design on different screen sizes
\ No newline at end of file
diff --git a/__tests__/api/fileStorage.test.ts b/__tests__/api/fileStorage.test.ts
new file mode 100644
index 00000000..ca12c5a2
--- /dev/null
+++ b/__tests__/api/fileStorage.test.ts
@@ -0,0 +1,30 @@
+import { describe, it, expect } from 'vitest';
+import ApiFetch from '@/lib/api-fetch';
+
+describe('FileStorage API', () => {
+ it('should fetch a list of files from /api/fileStorage/findMany', async () => {
+ const response = await ApiFetch.api.fileStorage.findMany.get();
+
+ expect(response.status).toBe(200);
+
+ const responseBody = response.data as any;
+
+ expect(responseBody.data).toBeInstanceOf(Array);
+ expect(responseBody.data.length).toBe(2);
+ expect(responseBody.data[0].name).toBe('file1.jpg');
+ });
+
+ it('should create a file using /api/fileStorage/create', async () => {
+ const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
+ const response = await ApiFetch.api.fileStorage.create.post({
+ file: mockFile,
+ name: 'hello.png',
+ });
+
+ expect(response.status).toBe(200);
+ const responseBody = response.data as any;
+
+ expect(responseBody.data.realName).toBe('hello.png');
+ expect(responseBody.data.id).toBe('3');
+ });
+});
diff --git a/__tests__/e2e/homepage.spec.ts b/__tests__/e2e/homepage.spec.ts
new file mode 100644
index 00000000..57b02234
--- /dev/null
+++ b/__tests__/e2e/homepage.spec.ts
@@ -0,0 +1,11 @@
+import { test, expect } from '@playwright/test';
+
+test('homepage has correct title and content', async ({ page }) => {
+ await page.goto('/');
+
+ // Wait for the redirect to /darmasaba
+ await page.waitForURL('/darmasaba');
+
+ // Check for the main heading
+ await expect(page.getByText('DARMASABA', { exact: true })).toBeVisible();
+});
diff --git a/__tests__/mocks/handlers.ts b/__tests__/mocks/handlers.ts
new file mode 100644
index 00000000..2854bf86
--- /dev/null
+++ b/__tests__/mocks/handlers.ts
@@ -0,0 +1,43 @@
+import { http, HttpResponse } from 'msw';
+
+export const handlers = [
+ http.get('http://localhost:3000/api/fileStorage/findMany', () => {
+ return HttpResponse.json({
+ data: [
+ { id: '1', name: 'file1.jpg', url: '/uploads/file1.jpg' },
+ { id: '2', name: 'file2.png', url: '/uploads/file2.png' },
+ ],
+ meta: {
+ page: 1,
+ limit: 10,
+ total: 2,
+ totalPages: 1,
+ },
+ });
+ }),
+ http.post('http://localhost:3000/api/fileStorage/create', async ({ request }) => {
+ const data = await request.formData();
+ const file = data.get('file') as File;
+ const name = data.get('name') as string;
+
+ if (!file) {
+ return new HttpResponse(null, { status: 400 });
+ }
+
+ return HttpResponse.json({
+ data: {
+ id: '3',
+ name: 'generated-nanoid',
+ path: `/uploads/generated-nanoid`,
+ link: `/uploads/generated-nanoid`,
+ realName: name,
+ mimeType: file.type,
+ category: "uncategorized",
+ isActive: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ deletedAt: null,
+ }
+ });
+ }),
+];
diff --git a/__tests__/mocks/server.ts b/__tests__/mocks/server.ts
new file mode 100644
index 00000000..e52fee0a
--- /dev/null
+++ b/__tests__/mocks/server.ts
@@ -0,0 +1,4 @@
+import { setupServer } from 'msw/node';
+import { handlers } from './handlers';
+
+export const server = setupServer(...handlers);
diff --git a/__tests__/setup.ts b/__tests__/setup.ts
new file mode 100644
index 00000000..83d8b89c
--- /dev/null
+++ b/__tests__/setup.ts
@@ -0,0 +1,7 @@
+import '@testing-library/jest-dom';
+import { server } from './mocks/server';
+import { beforeAll, afterEach, afterAll } from 'vitest';
+
+beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
diff --git a/bun.lockb b/bun.lockb
index a862a5a4..aef74337 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 065dc1c1..892c4639 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"scripts": {
"dev": "next dev",
"build": "next build",
- "start": "next start"
+ "start": "next start",
+ "test:api": "vitest run",
+ "test:e2e": "playwright test",
+ "test": "bun run test:api && bun run test:e2e"
},
"prisma": {
"seed": "bun run prisma/seed.ts"
@@ -106,17 +109,23 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+ "@playwright/test": "^1.58.2",
+ "@testing-library/jest-dom": "^6.9.1",
"@types/cli-progress": "^3.11.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@vitest/ui": "^4.0.18",
"eslint": "^9",
"eslint-config-next": "15.1.6",
+ "jsdom": "^28.0.0",
+ "msw": "^2.12.9",
"parcel": "^2.6.2",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
- "typescript": "^5"
+ "typescript": "^5",
+ "vitest": "^4.0.18"
}
}
diff --git a/playwright-report/data/3262232ac1998014dfaa14b6734778979a7c99c4.md b/playwright-report/data/3262232ac1998014dfaa14b6734778979a7c99c4.md
new file mode 100644
index 00000000..c0cf174c
--- /dev/null
+++ b/playwright-report/data/3262232ac1998014dfaa14b6734778979a7c99c4.md
@@ -0,0 +1,208 @@
+# Page snapshot
+
+```yaml
+- generic [active] [ref=e1]:
+ - generic [ref=e2]:
+ - generic [ref=e7]:
+ - button "Darmasaba Logo" [ref=e8] [cursor=pointer]:
+ - img "Darmasaba Logo" [ref=e10]
+ - button "PPID" [ref=e11] [cursor=pointer]:
+ - generic [ref=e13]: PPID
+ - button "Desa" [ref=e14] [cursor=pointer]:
+ - generic [ref=e16]: Desa
+ - button "Kesehatan" [ref=e17] [cursor=pointer]:
+ - generic [ref=e19]: Kesehatan
+ - button "Keamanan" [ref=e20] [cursor=pointer]:
+ - generic [ref=e22]: Keamanan
+ - button "Ekonomi" [ref=e23] [cursor=pointer]:
+ - generic [ref=e25]: Ekonomi
+ - button "Inovasi" [ref=e26] [cursor=pointer]:
+ - generic [ref=e28]: Inovasi
+ - button "Lingkungan" [ref=e29] [cursor=pointer]:
+ - generic [ref=e31]: Lingkungan
+ - button "Pendidikan" [ref=e32] [cursor=pointer]:
+ - generic [ref=e34]: Pendidikan
+ - button "Musik" [ref=e35] [cursor=pointer]:
+ - generic [ref=e37]: Musik
+ - button [ref=e38] [cursor=pointer]:
+ - img [ref=e40]
+ - generic [ref=e46]:
+ - generic [ref=e51]:
+ - generic [ref=e52]:
+ - generic [ref=e53]:
+ - img "Logo Darmasaba" [ref=e55]
+ - img "Logo Pudak" [ref=e57]
+ - generic [ref=e63]:
+ - generic [ref=e65]:
+ - generic [ref=e66]:
+ - img [ref=e67]
+ - paragraph [ref=e71]: Jam Operasional
+ - generic [ref=e72]:
+ - generic [ref=e74]: Buka
+ - paragraph [ref=e75]: 07:30 - 15:30
+ - generic [ref=e77]:
+ - generic [ref=e78]:
+ - img [ref=e79]
+ - paragraph [ref=e82]: Hari Ini
+ - generic [ref=e83]:
+ - paragraph [ref=e84]: Status Kantor
+ - paragraph [ref=e85]: Sedang Beroperasi
+ - paragraph [ref=e95]: Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
+ - generic [ref=e102]:
+ - generic [ref=e103]: Browser Anda tidak mendukung video.
+ - generic [ref=e106]:
+ - heading "Penghargaan Desa" [level=2] [ref=e107]
+ - paragraph [ref=e110]: Sedang memuat data penghargaan...
+ - button "Lihat semua penghargaan" [ref=e111] [cursor=pointer]:
+ - generic [ref=e112]:
+ - paragraph [ref=e114]: Lihat Semua Penghargaan
+ - img [ref=e116]
+ - generic [ref=e119]:
+ - generic [ref=e121]:
+ - heading "Layanan" [level=1] [ref=e122]
+ - paragraph [ref=e123]: Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!
+ - link "Detail" [ref=e125] [cursor=pointer]:
+ - /url: /darmasaba/desa/layanan
+ - generic [ref=e127]: Detail
+ - separator [ref=e129]
+ - generic [ref=e130]:
+ - generic [ref=e131]:
+ - paragraph [ref=e132]: Potensi Desa
+ - paragraph [ref=e133]: Jelajahi berbagai potensi dan peluang yang dimiliki desa. Fitur ini membantu warga maupun pemerintah desa dalam merencanakan dan mengembangkan program berbasis kekuatan lokal.
+ - paragraph [ref=e136]: Sedang memuat potensi desa...
+ - button "Lihat Semua Potensi" [ref=e139] [cursor=pointer]:
+ - generic [ref=e140]:
+ - generic [ref=e141]: Lihat Semua Potensi
+ - img [ref=e143]
+ - separator [ref=e146]
+ - generic [ref=e147]:
+ - generic [ref=e148]:
+ - paragraph [ref=e150]: Desa Anti Korupsi
+ - paragraph [ref=e151]: Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola secara terbuka dengan melibatkan warga dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan sesuai kebutuhan masyarakat.
+ - link "Selengkapnya" [ref=e153] [cursor=pointer]:
+ - /url: /darmasaba/desa-anti-korupsi/detail
+ - generic [ref=e155]: Selengkapnya
+ - paragraph [ref=e158]: Memuat Data...
+ - generic [ref=e166]:
+ - heading "SDGs Desa" [level=1] [ref=e168]
+ - paragraph [ref=e169]: SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
+ - generic [ref=e170]:
+ - generic [ref=e171]:
+ - img [ref=e172]
+ - paragraph [ref=e175]: Data SDGs Desa belum tersedia
+ - link "Jelajahi Semua Tujuan SDGs Desa" [ref=e177] [cursor=pointer]:
+ - /url: /darmasaba/sdgs-desa
+ - paragraph [ref=e180]: Jelajahi Semua Tujuan SDGs Desa
+ - generic [ref=e181]:
+ - generic [ref=e183]:
+ - heading "APBDes" [level=1] [ref=e184]
+ - paragraph [ref=e185]: Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.
+ - link "Lihat Semua Data" [ref=e187] [cursor=pointer]:
+ - /url: /darmasaba/apbdes
+ - generic [ref=e189]: Lihat Semua Data
+ - generic [ref=e191]:
+ - paragraph [ref=e193]: Pilih Tahun APBDes
+ - generic [ref=e194]:
+ - textbox "Pilih Tahun APBDes" [ref=e195]:
+ - /placeholder: Pilih tahun
+ - generic:
+ - img
+ - paragraph [ref=e197]: Tidak ada data APBDes untuk tahun yang dipilih.
+ - generic [ref=e202]:
+ - heading "Prestasi Desa" [level=1] [ref=e203]
+ - paragraph [ref=e204]: Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
+ - link "Lihat Semua Prestasi" [ref=e205] [cursor=pointer]:
+ - /url: /darmasaba/prestasi-desa
+ - generic [ref=e207]: Lihat Semua Prestasi
+ - button [ref=e211] [cursor=pointer]:
+ - img [ref=e214]
+ - button [ref=e219] [cursor=pointer]:
+ - img [ref=e221]
+ - generic [ref=e225]:
+ - contentinfo [ref=e228]:
+ - generic [ref=e230]:
+ - generic [ref=e231]:
+ - heading "Komitmen Layanan Kami" [level=2] [ref=e232]
+ - generic [ref=e233]:
+ - generic [ref=e234]:
+ - paragraph [ref=e235]: "1. Transparansi:"
+ - paragraph [ref=e236]: Pengelolaan dana desa dilakukan secara terbuka agar masyarakat dapat memahami dan memantau penggunaan anggaran.
+ - generic [ref=e237]:
+ - paragraph [ref=e238]: "2. Profesionalisme:"
+ - paragraph [ref=e239]: Layanan desa diberikan secara cepat, adil, dan profesional demi kepuasan masyarakat.
+ - generic [ref=e240]:
+ - paragraph [ref=e241]: "3. Partisipasi:"
+ - paragraph [ref=e242]: Masyarakat dilibatkan aktif dalam pengambilan keputusan demi pembangunan desa yang berhasil.
+ - generic [ref=e243]:
+ - paragraph [ref=e244]: "4. Inovasi:"
+ - paragraph [ref=e245]: Kami terus berinovasi, termasuk melalui teknologi, agar layanan semakin mudah diakses.
+ - generic [ref=e246]:
+ - paragraph [ref=e247]: "5. Keadilan:"
+ - paragraph [ref=e248]: Kebijakan dan program disusun untuk memberi manfaat yang merata bagi seluruh warga.
+ - generic [ref=e249]:
+ - paragraph [ref=e250]: "6. Pemberdayaan:"
+ - paragraph [ref=e251]: Masyarakat didukung melalui pelatihan, pendampingan, dan pengembangan usaha lokal.
+ - generic [ref=e252]:
+ - paragraph [ref=e253]: "7. Ramah Lingkungan:"
+ - paragraph [ref=e254]: Seluruh kegiatan pembangunan memperhatikan keberlanjutan demi menjaga alam dan kesehatan warga.
+ - separator [ref=e255]
+ - generic [ref=e256]:
+ - heading "Visi Kami" [level=2] [ref=e257]
+ - paragraph [ref=e258]: Dengan visi ini, kami berkomitmen menjadikan desa sebagai tempat yang aman, sejahtera, dan nyaman bagi seluruh warga.
+ - paragraph [ref=e259]: Kami percaya kemajuan dimulai dari kerja sama antara pemerintah desa dan masyarakat, didukung tata kelola yang baik demi kepentingan bersama. Saran maupun keluhan dapat disampaikan melalui kontak di bawah ini.
+ - generic [ref=e260]:
+ - paragraph [ref=e261]: "\"Desa Kuat, Warga Sejahtera!\""
+ - button "Logo Desa" [ref=e262] [cursor=pointer]:
+ - generic [ref=e263]:
+ - img "Logo Desa"
+ - generic [ref=e265]:
+ - generic [ref=e267]:
+ - paragraph [ref=e268]: Tentang Darmasaba
+ - paragraph [ref=e269]: Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
+ - generic [ref=e270]:
+ - link [ref=e271] [cursor=pointer]:
+ - /url: https://www.facebook.com/DarmasabaDesaku
+ - img [ref=e273]
+ - link [ref=e275] [cursor=pointer]:
+ - /url: https://www.instagram.com/ddarmasaba/
+ - img [ref=e277]
+ - link [ref=e280] [cursor=pointer]:
+ - /url: https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg
+ - img [ref=e282]
+ - link [ref=e285] [cursor=pointer]:
+ - /url: https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc
+ - img [ref=e287]
+ - generic [ref=e290]:
+ - paragraph [ref=e291]: Layanan Desa
+ - link "Administrasi Kependudukan" [ref=e292] [cursor=pointer]:
+ - /url: /darmasaba/desa/layanan/
+ - link "Layanan Sosial" [ref=e293] [cursor=pointer]:
+ - /url: /darmasaba/ekonomi/program-kemiskinan
+ - link "Pengaduan Masyarakat" [ref=e294] [cursor=pointer]:
+ - /url: /darmasaba/keamanan/laporan-publik
+ - link "Informasi Publik" [ref=e295] [cursor=pointer]:
+ - /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
+ - generic [ref=e297]:
+ - paragraph [ref=e298]: Tautan Penting
+ - link "Portal Badung" [ref=e299] [cursor=pointer]:
+ - /url: /darmasaba/desa/berita/semua
+ - link "E-Government" [ref=e300] [cursor=pointer]:
+ - /url: /darmasaba/inovasi/desa-digital-smart-village
+ - link "Transparansi" [ref=e301] [cursor=pointer]:
+ - /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
+ - generic [ref=e303]:
+ - paragraph [ref=e304]: Berlangganan Info
+ - paragraph [ref=e305]: Dapatkan kabar terbaru tentang program dan kegiatan desa langsung ke email Anda.
+ - generic [ref=e306]:
+ - generic [ref=e308]:
+ - textbox "Masukkan email Anda" [ref=e309]
+ - img [ref=e311]
+ - button "Daftar" [ref=e314] [cursor=pointer]:
+ - generic [ref=e316]: Daftar
+ - separator [ref=e317]
+ - paragraph [ref=e318]: © 2025 Desa Darmasaba. Hak cipta dilindungi.
+ - region "Notifications Alt+T"
+ - button "Open Next.js Dev Tools" [ref=e324] [cursor=pointer]:
+ - img [ref=e325]
+ - alert [ref=e328]
+```
\ No newline at end of file
diff --git a/playwright-report/index.html b/playwright-report/index.html
new file mode 100644
index 00000000..412d60b5
--- /dev/null
+++ b/playwright-report/index.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..c5c67f4c
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './__tests__/e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+ use: {
+ baseURL: 'http://localhost:3000',
+ trace: 'on-first-retry',
+ },
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+ webServer: {
+ command: 'bun run dev',
+ url: 'http://localhost:3000',
+ reuseExistingServer: !process.env.CI,
+ },
+});
diff --git a/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx
index 582a044d..d4730468 100644
--- a/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx
@@ -37,6 +37,15 @@ export default function EditKolaborasiInovasi() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.jumlah?.trim() !== '' &&
+ (formData.imageId?.trim() !== '' || file !== null)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
jumlah: "",
@@ -252,8 +261,11 @@ export default function EditKolaborasiInovasi() {
onClick={handleSubmit}
radius="md"
size="md"
+ disabled={!isFormValid() || isSubmitting}
style={{
- background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
+ background: !isFormValid() || isSubmitting
+ ? 'linear-gradient(135deg, #cccccc, #999999)'
+ : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
diff --git a/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx
index 19e4014b..14044c4c 100644
--- a/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx
@@ -19,6 +19,14 @@ function CreateSDGsDesa() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateSDGSDesa.create.form.name?.trim() !== '' &&
+ stateSDGSDesa.create.form.jumlah?.trim() !== '' &&
+ file !== null
+ );
+ };
useEffect(() => {
stateSDGSDesa.findMany.load();
@@ -203,8 +211,11 @@ function CreateSDGsDesa() {
onClick={handleSubmit}
radius="md"
size="md"
+ disabled={!isFormValid() || isSubmitting}
style={{
- background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
+ background: !isFormValid() || isSubmitting
+ ? 'linear-gradient(135deg, #cccccc, #999999)'
+ : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
index ad5075dd..e22b2617 100644
--- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
@@ -53,6 +53,14 @@ function EditAPBDes() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ apbdesState.edit.form.items.length > 0
+ );
+ };
+
const [previewImage, setPreviewImage] = useState(null);
const [previewDoc, setPreviewDoc] = useState(null);
const [imageFile, setImageFile] = useState(null);
@@ -492,9 +500,11 @@ function EditAPBDes() {