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() {