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/.gitignore b/.gitignore
index ebd64b35..2f3afc79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,9 @@ yarn-error.log*
# env
.env*
+# QC
+QC
+
# vercel
.vercel
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..7f208dd4 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/darkMode.md b/darkMode.md
new file mode 100644
index 00000000..713ac5db
--- /dev/null
+++ b/darkMode.md
@@ -0,0 +1,169 @@
+# 🌙 Dark Mode Design Specification
+## Admin Darmasaba – Dashboard & CMS
+
+Dokumen ini mendefinisikan standar **Dark Mode UI** agar:
+- nyaman di mata
+- konsisten
+- tidak flat
+- tetap profesional untuk aplikasi pemerintahan
+
+---
+
+## 🎨 Color Palette (Dark Mode)
+
+### Background Layers
+| Layer | Token | Warna | Fungsi |
+|------|------|------|------|
+| Base | `--bg-base` | `#0B1220` | Background utama aplikasi |
+| App | `--bg-app` | `#0F172A` | Area kerja utama |
+| Card | `--bg-card` | `#162235` | Card / container |
+| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input |
+
+---
+
+### Border & Divider
+| Token | Warna | Catatan |
+|-----|------|--------|
+| `--border-default` | `#2A3A52` | Border utama |
+| `--border-soft` | `#22314A` | Divider halus |
+
+> ❗ Hindari border terlalu tipis (`opacity < 20%`)
+
+---
+
+### Text Colors
+| Jenis | Token | Warna |
+|-----|------|------|
+| Primary | `--text-primary` | `#E5E7EB` |
+| Secondary | `--text-secondary` | `#9CA3AF` |
+| Muted | `--text-muted` | `#6B7280` |
+| Inverse | `--text-inverse` | `#020617` |
+
+---
+
+### Accent & Action
+| Fungsi | Warna |
+|------|------|
+| Primary Action | `#3B82F6` |
+| Hover | `#2563EB` |
+| Active | `#1D4ED8` |
+| Link | `#60A5FA` |
+
+---
+
+### Status Colors
+| Status | Warna |
+|------|------|
+| Success | `#22C55E` |
+| Warning | `#FACC15` |
+| Error | `#EF4444` |
+| Info | `#38BDF8` |
+
+---
+
+## 🧱 Layout Rules
+
+### Sidebar
+- Background: `--bg-app`
+- Active menu:
+ - Background: `rgba(59,130,246,0.15)`
+ - Text: Primary
+ - Indicator: kiri (2–3px accent bar)
+- Hover:
+ - Background: `rgba(255,255,255,0.04)`
+
+---
+
+### Header / Topbar
+- Background: `linear-gradient(#0F172A → #0B1220)`
+- Border bawah wajib (`--border-soft`)
+- Icon:
+ - Default: muted
+ - Hover: primary
+
+---
+
+## 📦 Card & Section
+
+### Card
+- Background: `--bg-card`
+- Border: `--border-default`
+- Radius: 12–16px
+- Jangan pakai shadow hitam
+
+### Section Header
+- Font weight lebih besar
+- Text: primary
+- Spacing jelas dari konten
+
+---
+
+## 📊 Table (Dark Mode Friendly)
+
+### Table Header
+- Background: `--bg-surface`
+- Text: secondary
+- Font weight: medium
+
+### Table Row
+- Default: transparent
+- Hover:
+ - Background: `rgba(255,255,255,0.03)`
+- Divider antar row wajib terlihat
+
+### Link di Table
+- Warna link **lebih terang dari text**
+- Hover underline
+
+---
+
+## 🔘 Button Rules
+
+### Primary Button
+- Background: Primary Action
+- Text: Inverse
+- Hover: darker shade
+
+### Secondary Button
+- Background: transparent
+- Border: `--border-default`
+- Text: primary
+
+### Icon Button
+- Default: muted
+- Hover: primary + bg soft
+
+---
+
+## 🧭 Tab Navigation
+
+- Inactive:
+ - Text: muted
+- Active:
+ - Background: `rgba(59,130,246,0.15)`
+ - Text: primary
+ - Icon ikut berubah
+
+---
+
+## 🌗 Dark vs Light Mode Rule
+- Layout, spacing, typography **HARUS SAMA**
+- Yang boleh beda:
+ - warna
+ - border intensity
+ - background layer
+
+> ❌ Jangan ganti struktur UI antara dark & light
+
+---
+
+## ✅ Dark Mode Checklist
+- [ ] Kontras teks terbaca
+- [ ] Active state jelas
+- [ ] Hover terasa hidup
+- [ ] Tidak flat
+- [ ] Tidak silau
+
+---
+
+Dokumen ini adalah **single source of truth** untuk Dark Mode.
\ No newline at end of file
diff --git a/package.json b/package.json
index 065dc1c1..61958b92 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"
@@ -59,6 +62,7 @@
"colors": "^1.4.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
+ "dompurify": "^3.3.1",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel": "^8.6.0",
@@ -106,17 +110,24 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+ "@playwright/test": "^1.58.2",
+ "@testing-library/jest-dom": "^6.9.1",
"@types/cli-progress": "^3.11.6",
+ "@types/dompurify": "^3.2.0",
"@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/prisma/migrations/20260225082505_deploy/migration.sql b/prisma/migrations/20260225082505_deploy/migration.sql
new file mode 100644
index 00000000..ac4024a7
--- /dev/null
+++ b/prisma/migrations/20260225082505_deploy/migration.sql
@@ -0,0 +1,170 @@
+/*
+ Warnings:
+
+ - You are about to alter the column `nama` on the `KategoriPotensi` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
+ - You are about to alter the column `name` on the `PotensiDesa` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
+ - You are about to alter the column `kategoriId` on the `PotensiDesa` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(36)`.
+ - A unique constraint covering the columns `[nama]` on the table `KategoriPotensi` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[name]` on the table `PotensiDesa` will be added. If there are existing duplicate values, this will fail.
+ - Made the column `kategoriId` on table `PotensiDesa` required. This step will fail if there are existing NULL values in that column.
+
+*/
+-- DropForeignKey
+ALTER TABLE "DataPerpustakaan" DROP CONSTRAINT "DataPerpustakaan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "DesaDigital" DROP CONSTRAINT "DesaDigital_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "InfoTekno" DROP CONSTRAINT "InfoTekno_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KegiatanDesa" DROP CONSTRAINT "KegiatanDesa_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PengaduanMasyarakat" DROP CONSTRAINT "PengaduanMasyarakat_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_kategoriId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "ProfileDesaImage" DROP CONSTRAINT "ProfileDesaImage_imageId_fkey";
+
+-- AlterTable
+ALTER TABLE "CaraMemperolehInformasi" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "CaraMemperolehSalinanInformasi" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "DaftarInformasiPublik" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "DasarHukumPPID" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "DataPerpustakaan" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "DesaDigital" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "FormulirPermohonanKeberatan" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "InfoTekno" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "JenisInformasiDiminta" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "JenisKelaminResponden" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "KategoriPotensi" ALTER COLUMN "nama" SET DATA TYPE VARCHAR(100),
+ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "KategoriPrestasiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "KegiatanDesa" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "LambangDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "MaskotDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "PegawaiPPID" ADD COLUMN "deletedAt" TIMESTAMP(3);
+
+-- AlterTable
+ALTER TABLE "PengaduanMasyarakat" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PermohonanInformasiPublik" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "PilihanRatingResponden" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "PosisiOrganisasiPPID" ADD COLUMN "deletedAt" TIMESTAMP(3);
+
+-- AlterTable
+ALTER TABLE "PotensiDesa" ALTER COLUMN "name" SET DATA TYPE VARCHAR(255),
+ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT,
+ALTER COLUMN "kategoriId" SET NOT NULL,
+ALTER COLUMN "kategoriId" SET DATA TYPE VARCHAR(36);
+
+-- AlterTable
+ALTER TABLE "PrestasiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "ProfileDesaImage" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "ProfilePPID" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "Responden" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "SejarahDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "UmurResponden" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "VisiMisiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- AlterTable
+ALTER TABLE "VisiMisiPPID" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KategoriPotensi_nama_key" ON "KategoriPotensi"("nama");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PotensiDesa_name_key" ON "PotensiDesa"("name");
+
+-- AddForeignKey
+ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DesaDigital" ADD CONSTRAINT "DesaDigital_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InfoTekno" ADD CONSTRAINT "InfoTekno_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
index 648c57fd..044d57cd 100644
--- a/prisma/migrations/migration_lock.toml
+++ b/prisma/migrations/migration_lock.toml
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
-provider = "postgresql"
\ No newline at end of file
+provider = "postgresql"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index ac06a3a0..5a9168cd 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -236,7 +236,7 @@ model PrestasiDesa {
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -245,7 +245,7 @@ model KategoriPrestasiDesa {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
PrestasiDesa PrestasiDesa[]
}
@@ -263,7 +263,7 @@ model Responden {
kelompokUmurId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -272,7 +272,7 @@ model JenisKelaminResponden {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
Responden Responden[]
}
@@ -282,7 +282,7 @@ model PilihanRatingResponden {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
Responden Responden[]
}
@@ -292,7 +292,7 @@ model UmurResponden {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
Responden Responden[]
}
@@ -326,6 +326,7 @@ model PosisiOrganisasiPPID {
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ deletedAt DateTime?
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@ -345,6 +346,7 @@ model PegawaiPPID {
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ deletedAt DateTime?
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@ -370,7 +372,7 @@ model VisiMisiPPID {
misi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -381,7 +383,7 @@ model DasarHukumPPID {
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -398,7 +400,7 @@ model ProfilePPID {
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -410,7 +412,7 @@ model DaftarInformasiPublik {
tanggal DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -431,7 +433,7 @@ model PermohonanInformasiPublik {
caraMemperolehSalinanInformasiId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -440,7 +442,7 @@ model JenisInformasiDiminta {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
PermohonanInformasiPublik PermohonanInformasiPublik[]
}
@@ -450,7 +452,7 @@ model CaraMemperolehInformasi {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
PermohonanInformasiPublik PermohonanInformasiPublik[]
}
@@ -460,7 +462,7 @@ model CaraMemperolehSalinanInformasi {
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
PermohonanInformasiPublik PermohonanInformasiPublik[]
}
@@ -474,7 +476,7 @@ model FormulirPermohonanKeberatan {
alasan String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -531,7 +533,7 @@ model SejarahDesa {
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -541,7 +543,7 @@ model VisiMisiDesa {
misi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -551,7 +553,7 @@ model LambangDesa {
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -562,7 +564,7 @@ model MaskotDesa {
images ProfileDesaImage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
@@ -631,25 +633,25 @@ model KategoriBerita {
// ========================================= POTENSI DESA ========================================= //
model PotensiDesa {
id String @id @default(cuid())
- name String
- deskripsi String
+ name String @unique @db.VarChar(255)
+ deskripsi String @db.Text
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
- kategoriId String?
+ kategoriId String @db.VarChar(36)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
}
model KategoriPotensi {
id String @id @default(cuid())
- nama String
+ nama String @unique @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime?
isActive Boolean @default(true)
PotensiDesa PotensiDesa[]
}
diff --git a/src/app/admin/(dashboard)/_com/header.tsx b/src/app/admin/(dashboard)/_com/header.tsx
index 39735f4d..0b7c345b 100644
--- a/src/app/admin/(dashboard)/_com/header.tsx
+++ b/src/app/admin/(dashboard)/_com/header.tsx
@@ -1,7 +1,11 @@
+'use client';
+
import React from 'react';
-import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core';
+import { Grid, GridCol, Paper, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
-import colors from '@/con/colors';
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { UnifiedTitle } from '@/components/admin/UnifiedTypography';
type HeaderSearchProps = {
title: string;
@@ -18,13 +22,16 @@ const HeaderSearch = ({
value,
onChange,
}: HeaderSearchProps) => {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
return (
- {title}
+ {title}
-
+
diff --git a/src/app/admin/(dashboard)/_com/judulList.tsx b/src/app/admin/(dashboard)/_com/judulList.tsx
index 4eaa5731..7f376d11 100644
--- a/src/app/admin/(dashboard)/_com/judulList.tsx
+++ b/src/app/admin/(dashboard)/_com/judulList.tsx
@@ -1,12 +1,16 @@
'use client'
-import colors from '@/con/colors';
-import { Grid, GridCol, Button, Text } from '@mantine/core';
+import { Grid, GridCol, Button } from '@mantine/core';
import { IconCircleDashedPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { UnifiedText } from '@/components/admin/UnifiedTypography';
const JudulList = ({ title = "", href = "#" }) => {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
const router = useRouter();
const handleNavigate = () => {
@@ -16,10 +20,18 @@ const JudulList = ({ title = "", href = "#" }) => {
return (
- {title}
+ {title}
-
diff --git a/src/app/admin/(dashboard)/_com/judulListTab.tsx b/src/app/admin/(dashboard)/_com/judulListTab.tsx
index 21037671..dda1fe69 100644
--- a/src/app/admin/(dashboard)/_com/judulListTab.tsx
+++ b/src/app/admin/(dashboard)/_com/judulListTab.tsx
@@ -1,9 +1,11 @@
'use client'
-import colors from '@/con/colors';
-import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core';
+import { Grid, GridCol, Button, Paper, TextInput } from '@mantine/core';
import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { UnifiedText } from '@/components/admin/UnifiedTypography';
type JudulListTabProps = {
title: string;
@@ -14,17 +16,16 @@ type JudulListTabProps = {
onChange?: (e: React.ChangeEvent) => void;
}
-
-
-
const JudulListTab = ({
title = "",
href = "#",
placeholder = "pencarian",
searchIcon = ,
value,
- onChange
+ onChange
}: JudulListTabProps) => {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
const router = useRouter();
const handleNavigate = () => {
@@ -34,10 +35,17 @@ const JudulListTab = ({
return (
- {title}
+
+ {title}
+
-
+
-
+
diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
index df9cde1d..9b5cb438 100644
--- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
+++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
@@ -38,11 +38,9 @@ function normalizeItem(item: Partial>): z.infer
const anggaran = item.anggaran ?? 0;
const realisasi = item.realisasi ?? 0;
-
-
// ✅ Formula yang benar
- const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget
+ const selisih = realisasi - anggaran; // positif = sisa anggaran, negatif = over budget
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
return {
diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts
index d4abaf81..152f16d7 100644
--- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts
+++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts
@@ -55,10 +55,15 @@ const programInovasi = proxy({
programInovasi.findMany.load();
return toast.success("Sukses menambahkan");
}
- console.log(res);
+ if (process.env.NODE_ENV === 'development') {
+ console.log(res);
+ }
return toast.error("failed create");
} catch (error) {
- console.log((error as Error).message);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Create error:", error);
+ }
+ toast.error("Gagal menambahkan data");
} finally {
programInovasi.create.loading = false;
}
@@ -91,13 +96,17 @@ const programInovasi = proxy({
programInovasi.findMany.total = res.data.total || 0;
programInovasi.findMany.totalPages = res.data.totalPages || 1;
} else {
- console.error("Failed to load pegawai:", res.data?.message);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Failed to load pegawai:", res.data?.message);
+ }
programInovasi.findMany.data = [];
programInovasi.findMany.total = 0;
programInovasi.findMany.totalPages = 1;
}
} catch (error) {
- console.error("Error loading pegawai:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error loading pegawai:", error);
+ }
programInovasi.findMany.data = [];
programInovasi.findMany.total = 0;
programInovasi.findMany.totalPages = 1;
@@ -112,19 +121,25 @@ const programInovasi = proxy({
image: true;
};
}> | null,
+ loading: false,
async load(id: string) {
try {
- const res = await fetch(`/api/landingpage/programinovasi/${id}`);
- if (res.ok) {
- const data = await res.json();
- programInovasi.findUnique.data = data.data ?? null;
+ programInovasi.findUnique.loading = true;
+ const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get();
+ if (res.data?.success) {
+ programInovasi.findUnique.data = res.data.data ?? null;
+ return res.data.data;
} else {
- console.error("Failed to fetch program inovasi:", res.statusText);
+ toast.error(res.data?.message || "Gagal memuat data program inovasi");
programInovasi.findUnique.data = null;
+ return null;
}
} catch (error) {
console.error("Error fetching program inovasi:", error);
programInovasi.findUnique.data = null;
+ return null;
+ } finally {
+ programInovasi.findUnique.loading = false;
}
},
},
@@ -135,27 +150,18 @@ const programInovasi = proxy({
try {
programInovasi.delete.loading = true;
+ const res = await (ApiFetch.api.landingpage.programinovasi as any)["del"][id].delete();
- const response = await fetch(
- `/api/landingpage/programinovasi/del/${id}`,
- {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
-
- const result = await response.json();
-
- if (response.ok && result?.success) {
- toast.success(result.message || "Program inovasi berhasil dihapus");
- await programInovasi.findMany.load(); // refresh list
+ if (res.data?.success) {
+ toast.success(res.data.message || "Program inovasi berhasil dihapus");
+ await programInovasi.findMany.load();
} else {
- toast.error(result?.message || "Gagal menghapus program inovasi");
+ toast.error(res.data?.message || "Gagal menghapus program inovasi");
}
} catch (error) {
- console.error("Gagal delete:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Gagal delete:", error);
+ }
toast.error("Terjadi kesalahan saat menghapus program inovasi");
} finally {
programInovasi.delete.loading = false;
@@ -174,20 +180,11 @@ const programInovasi = proxy({
}
try {
- const response = await fetch(`/api/landingpage/programinovasi/${id}`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
+ programInovasi.update.loading = true;
+ const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get();
- const result = await response.json();
-
- if (result?.success) {
- const data = result.data;
+ if (res.data?.success) {
+ const data = res.data.data;
this.id = data.id;
this.form = {
name: data.name,
@@ -197,13 +194,15 @@ const programInovasi = proxy({
};
return data;
} else {
- throw new Error(
- result?.message || "Gagal mengambil data program inovasi"
- );
+ toast.error(res.data?.message || "Gagal mengambil data program inovasi");
+ return null;
}
} catch (error) {
- console.error((error as Error).message);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error loading program inovasi:", error);
+ }
toast.error("Terjadi kesalahan saat mengambil data program inovasi");
+ return null;
} finally {
programInovasi.update.loading = false;
}
@@ -221,41 +220,25 @@ const programInovasi = proxy({
try {
programInovasi.update.loading = true;
+ const res = await (ApiFetch.api.landingpage.programinovasi as any)[this.id].put({
+ name: this.form.name,
+ description: this.form.description,
+ imageId: this.form.imageId,
+ link: this.form.link,
+ });
- const response = await fetch(
- `/api/landingpage/programinovasi/${this.id}`,
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- name: this.form.name,
- description: this.form.description,
- imageId: this.form.imageId,
- link: this.form.link,
- }),
- }
- );
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- throw new Error(
- errorData.message || `HTTP error! status: ${response.status}`
- );
- }
-
- const result = await response.json();
-
- if (result.success) {
+ if (res.data?.success) {
toast.success("Berhasil update program inovasi");
- await programInovasi.findMany.load(); // refresh list
+ await programInovasi.findMany.load();
return true;
} else {
- throw new Error(result.message || "Gagal update program inovasi");
+ toast.error(res.data?.message || "Gagal update program inovasi");
+ return false;
}
} catch (error) {
- console.error("Error updating program inovasi:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error updating program inovasi:", error);
+ }
toast.error(
error instanceof Error
? error.message
@@ -443,7 +426,7 @@ const pejabatDesa = proxy({
const templateMediaSosial = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().nullable().optional(),
- iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
+ iconUrl: z.string().optional(), // ✅ Optional - tidak selalu required
icon: z.string().nullable().optional(),
});
@@ -484,10 +467,15 @@ const mediaSosial = proxy({
mediaSosial.findMany.load();
return toast.success("Sukses menambahkan");
}
- console.log(res);
+ if (process.env.NODE_ENV === 'development') {
+ console.log(res);
+ }
return toast.error("failed create");
} catch (error) {
- console.log((error as Error).message);
+ if (process.env.NODE_ENV === 'development') {
+ console.log((error as Error).message);
+ }
+ toast.error("Gagal menambahkan data");
} finally {
mediaSosial.create.loading = false;
}
@@ -518,13 +506,17 @@ const mediaSosial = proxy({
mediaSosial.findMany.total = res.data.total || 0;
mediaSosial.findMany.totalPages = res.data.totalPages || 1;
} else {
- console.error("Failed to load media sosial:", res.data?.message);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Failed to load media sosial:", res.data?.message);
+ }
mediaSosial.findMany.data = [];
mediaSosial.findMany.total = 0;
mediaSosial.findMany.totalPages = 1;
}
} catch (error) {
- console.error("Error loading media sosial:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error loading media sosial:", error);
+ }
mediaSosial.findMany.data = [];
mediaSosial.findMany.total = 0;
mediaSosial.findMany.totalPages = 1;
@@ -539,25 +531,32 @@ const mediaSosial = proxy({
image: true;
};
}> | null,
+ loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
- mediaSosial.update.loading = true;
+ mediaSosial.findUnique.loading = true;
try {
- const res = await fetch(`/api/landingpage/mediasosial/${id}`);
- if (res.ok) {
- const data = await res.json();
- mediaSosial.findUnique.data = data.data ?? null;
+ const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get();
+ if (res.data?.success) {
+ mediaSosial.findUnique.data = res.data.data ?? null;
+ return res.data.data;
} else {
- console.error("Failed to fetch media sosial:", res.statusText);
+ toast.error(res.data?.message || "Gagal memuat data media sosial");
mediaSosial.findUnique.data = null;
+ return null;
}
} catch (error) {
- console.error("Error fetching media sosial:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error fetching media sosial:", error);
+ }
mediaSosial.findUnique.data = null;
+ return null;
+ } finally {
+ mediaSosial.findUnique.loading = false;
}
},
},
@@ -568,24 +567,18 @@ const mediaSosial = proxy({
try {
mediaSosial.delete.loading = true;
+ const res = await (ApiFetch.api.landingpage.mediasosial as any)["del"][id].delete();
- const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- const result = await response.json();
-
- if (response.ok && result?.success) {
- toast.success(result.message || "Media Sosial berhasil dihapus");
- await mediaSosial.findMany.load(); // refresh list
+ if (res.data?.success) {
+ toast.success(res.data.message || "Media Sosial berhasil dihapus");
+ await mediaSosial.findMany.load();
} else {
- toast.error(result?.message || "Gagal menghapus media sosial");
+ toast.error(res.data?.message || "Gagal menghapus media sosial");
}
} catch (error) {
- console.error("Gagal delete:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Gagal delete:", error);
+ }
toast.error("Terjadi kesalahan saat menghapus media sosial");
} finally {
mediaSosial.delete.loading = false;
@@ -603,43 +596,32 @@ const mediaSosial = proxy({
return null;
}
- mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
-
+ mediaSosial.update.loading = true;
try {
- const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- });
+ const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get();
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const result = await response.json();
-
- if (result?.success) {
- const data = result.data;
+ if (res.data?.success) {
+ const data = res.data.data;
this.id = data.id;
this.form = {
name: data.name || "",
imageId: data.imageId || null,
iconUrl: data.iconUrl || "",
icon: data.icon || null,
-
};
return data;
} else {
- throw new Error(
- result?.message || "Gagal mengambil data media sosial"
- );
+ toast.error(res.data?.message || "Gagal mengambil data media sosial");
+ return null;
}
} catch (error) {
- console.error((error as Error).message);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error loading media sosial:", error);
+ }
toast.error("Terjadi kesalahan saat mengambil data media sosial");
+ return null;
} finally {
- mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
+ mediaSosial.update.loading = false;
}
},
@@ -655,41 +637,25 @@ const mediaSosial = proxy({
try {
mediaSosial.update.loading = true;
+ const res = await (ApiFetch.api.landingpage.mediasosial as any)[this.id].put({
+ name: this.form.name,
+ imageId: this.form.imageId,
+ iconUrl: this.form.iconUrl,
+ icon: this.form.icon,
+ });
- const response = await fetch(
- `/api/landingpage/mediasosial/${this.id}`,
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- name: this.form.name,
- imageId: this.form.imageId,
- iconUrl: this.form.iconUrl,
- icon: this.form.icon,
- }),
- }
- );
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- throw new Error(
- errorData.message || `HTTP error! status: ${response.status}`
- );
- }
-
- const result = await response.json();
-
- if (result.success) {
+ if (res.data?.success) {
toast.success("Berhasil update media sosial");
- await mediaSosial.findMany.load(); // refresh list
+ await mediaSosial.findMany.load();
return true;
} else {
- throw new Error(result.message || "Gagal update media sosial");
+ toast.error(res.data?.message || "Gagal update media sosial");
+ return false;
}
} catch (error) {
- console.error("Error updating media sosial:", error);
+ if (process.env.NODE_ENV === 'development') {
+ console.error("Error updating media sosial:", error);
+ }
toast.error(
error instanceof Error
? error.message
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
index 6f89e888..dfb687c4 100644
--- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
@@ -33,6 +33,13 @@ function EditKategoriBerita() {
name: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== ''
+ );
+ };
+
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -72,6 +79,11 @@ function EditKategoriBerita() {
};
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama kategori berita wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// update global state hanya saat submit
@@ -143,8 +155,11 @@ function EditKategoriBerita() {
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, #eeeeee)`
+ : `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)/desa/berita/kategori-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
index f6957fb0..c047ccfa 100644
--- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
@@ -22,6 +22,13 @@ function CreateKategoriBerita() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.name?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
createState.create.form = {
name: '',
@@ -29,6 +36,11 @@ function CreateKategoriBerita() {
};
const handleSubmit = async () => {
+ if (!createState.create.form.name?.trim()) {
+ toast.error('Nama kategori berita wajib diisi');
+ return;
+ }
+
setIsSubmitting(true);
try {
await createState.create.create();
@@ -93,8 +105,11 @@ function CreateKategoriBerita() {
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, #eeeeee)`
+ : `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)/desa/berita/kategori-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
index 82be57ec..5f6d7a78 100644
--- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
@@ -160,7 +160,7 @@ function ListKategoriBerita({ search }: { search: string }) {
))
) : (
-
+ {/* ✅ Match column count (3 columns) */}
Tidak ada data kategori berita yang cocok
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
index 101c57bd..414e2749 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
@@ -48,6 +48,24 @@ function EditBerita() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ formData.kategoriBeritaId !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ (file !== null || originalData.imageId !== '') && // Either a new file is selected or an existing image exists
+ !isHtmlEmpty(formData.content)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
@@ -103,6 +121,31 @@ function EditBerita() {
};
const handleSubmit = async () => {
+ if (!formData.judul?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!formData.kategoriBeritaId) {
+ toast.error('Kategori wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi singkat wajib diisi');
+ return;
+ }
+
+ if (!file && !originalData.imageId) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.content)) {
+ toast.error('Konten wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Update global state hanya sekali di sini
@@ -326,8 +369,11 @@ function EditBerita() {
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, #eeeeee)`
+ : `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)/desa/berita/list-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
index 99938ff1..0ae98722 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
@@ -32,6 +32,24 @@ export default function CreateBerita() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ beritaState.berita.create.form.judul?.trim() !== '' &&
+ beritaState.berita.create.form.kategoriBeritaId !== '' &&
+ !isHtmlEmpty(beritaState.berita.create.form.deskripsi) &&
+ file !== null &&
+ !isHtmlEmpty(beritaState.berita.create.form.content)
+ );
+ };
+
useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load();
}, []);
@@ -49,6 +67,31 @@ export default function CreateBerita() {
};
const handleSubmit = async () => {
+ if (!beritaState.berita.create.form.judul?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!beritaState.berita.create.form.kategoriBeritaId) {
+ toast.error('Kategori wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(beritaState.berita.create.form.deskripsi)) {
+ toast.error('Deskripsi singkat wajib diisi');
+ return;
+ }
+
+ if (!file) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(beritaState.berita.create.form.content)) {
+ toast.error('Konten wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!file) {
@@ -250,8 +293,11 @@ export default function CreateBerita() {
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, #eeeeee)`
+ : `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)/desa/berita/list-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
index 97dc4d11..c36447f6 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
@@ -187,7 +187,7 @@ function ListBerita({ search }: { search: string }) {
{
- load(newPage, 10);
+ load(newPage, 10, debouncedSearch); // ✅ Include search parameter
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
index 21dd4231..741e0b99 100644
--- a/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
@@ -45,6 +45,22 @@ function EditFoto() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ (file !== null || originalData.imagesId !== '') // Either a new file is selected or an existing image exists
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
@@ -94,6 +110,21 @@ function EditFoto() {
};
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (!file && !originalData.imagesId) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Update global state hanya sekali di sini
@@ -285,8 +316,11 @@ function EditFoto() {
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, #eeeeee)`
+ : `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)/desa/gallery/foto/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
index 72193ec5..cf076af2 100644
--- a/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
@@ -30,6 +30,22 @@ function CreateFoto() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ FotoState.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(FotoState.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
FotoState.create.form = {
name: '',
@@ -41,6 +57,21 @@ function CreateFoto() {
};
const handleSubmit = async () => {
+ if (!FotoState.create.form.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(FotoState.create.form.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (!file) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!file) {
@@ -210,8 +241,11 @@ function CreateFoto() {
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, #eeeeee)`
+ : `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)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
index a6647bd5..40dfb76f 100644
--- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
@@ -28,6 +28,24 @@ function EditVideo() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
+ return (
+ formData.name?.trim() !== '' &&
+ formData.linkVideo?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ embedLink !== null // Make sure the embed link is valid
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
@@ -86,6 +104,21 @@ function EditVideo() {
};
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!formData.linkVideo?.trim()) {
+ toast.error('Link YouTube wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
@@ -218,8 +251,11 @@ function EditVideo() {
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, #eeeeee)`
+ : `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)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
index 6896221f..ae0f3870 100644
--- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
@@ -28,6 +28,23 @@ function CreateVideo() {
const embedLink = convertYoutubeUrlToEmbed(link);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ videoState.create.form.name?.trim() !== '' &&
+ link.trim() !== '' &&
+ !isHtmlEmpty(videoState.create.form.deskripsi) &&
+ embedLink !== null // Make sure the embed link is valid
+ );
+ };
+
const resetForm = () => {
videoState.create.form = {
name: '',
@@ -38,6 +55,26 @@ function CreateVideo() {
};
const handleSubmit = async () => {
+ if (!videoState.create.form.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!link.trim()) {
+ toast.error('Link YouTube wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(videoState.create.form.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (!embedLink) {
+ toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!embedLink) {
@@ -168,8 +205,11 @@ function CreateVideo() {
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, #eeeeee)`
+ : `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)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
index 6ebb654a..3b3a6e81 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
@@ -35,6 +35,21 @@ function EditPelayananPendudukNonPermanent() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: '',
deskripsi: '',
@@ -86,6 +101,16 @@ function EditPelayananPendudukNonPermanent() {
};
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!statePendudukNonPermanent.findById.data) return;
@@ -173,8 +198,11 @@ function EditPelayananPendudukNonPermanent() {
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, #eeeeee)`
+ : `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)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
index d354e203..6c10fc9f 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
@@ -36,6 +36,22 @@ function EditPelayananPerizinanBerusaha() {
});
const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
id: '',
name: '',
@@ -102,6 +118,21 @@ function EditPelayananPerizinanBerusaha() {
};
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (!formData.link?.trim()) {
+ toast.error('Link wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await state.update.update(formData);
@@ -192,8 +223,11 @@ function EditPelayananPerizinanBerusaha() {
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, #eeeeee)`
+ : `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)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx
index 3e1adf6a..938488f2 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx
@@ -149,6 +149,22 @@ function EditSuratKeterangan() {
const [previewImage2, setPreviewImage2] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ (previewImage !== null || originalData.imageId !== '') // Either a new file is selected or an existing image exists
+ );
+ };
+
// 🧭 Load Initial Data
useEffect(() => {
const loadSurat = async () => {
@@ -209,6 +225,21 @@ function EditSuratKeterangan() {
// 💾 Submit Handler
const handleSubmit = useCallback(async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama surat keterangan wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Konten wajib diisi');
+ return;
+ }
+
+ if (!previewImage && !originalData.imageId) {
+ toast.error('Gambar konten pelayanan wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
@@ -251,7 +282,7 @@ function EditSuratKeterangan() {
} finally {
setIsSubmitting(false);
}
- }, [formData, file, file2, router]);
+ }, [formData, file, file2, router, previewImage, originalData.imageId]);
// 📝 Form Field Handlers
const handleNameChange = (e: React.ChangeEvent) => {
@@ -324,10 +355,10 @@ function EditSuratKeterangan() {
{/* Action Buttons */}
-
@@ -336,9 +367,11 @@ function EditSuratKeterangan() {
{
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateSurat.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(stateSurat.create.form.deskripsi) &&
+ previewImage !== null
+ );
+ };
+
const resetForm = () => {
stateSurat.create.form = {
name: '',
@@ -43,8 +59,19 @@ function CreateSuratKeterangan() {
};
const handleSubmit = async () => {
+ if (!stateSurat.create.form.name?.trim()) {
+ toast.error('Nama surat keterangan wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateSurat.create.form.deskripsi)) {
+ toast.error('Konten wajib diisi');
+ return;
+ }
+
if (!previewImage) {
- return toast.warn('Pilih file gambar utama terlebih dahulu');
+ toast.error('Gambar konten pelayanan wajib dipilih');
+ return;
}
try {
@@ -284,8 +311,11 @@ function CreateSuratKeterangan() {
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, #eeeeee)`
+ : `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)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
index eeb2d53e..e73be70a 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
@@ -24,6 +24,15 @@ function EditPelayananTelunjukSakti() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.deskripsi?.trim() !== '' &&
+ formData.link?.trim() !== ''
+ );
+ };
+
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
@@ -84,6 +93,21 @@ function EditPelayananTelunjukSakti() {
// Submit: update global state hanya saat simpan
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama pelayanan wajib diisi');
+ return;
+ }
+
+ if (!formData.deskripsi?.trim()) {
+ toast.error('Judul link wajib diisi');
+ return;
+ }
+
+ if (!formData.link?.trim()) {
+ toast.error('Link wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
stateTelunjukDesa.edit.form = {
@@ -165,8 +189,11 @@ function EditPelayananTelunjukSakti() {
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, #eeeeee)`
+ : `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)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
index dd361915..f96b381a 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
@@ -23,6 +23,15 @@ function CreatePelayananTelunjukDesa() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateTelunjukDesa.create.form.name?.trim() !== '' &&
+ stateTelunjukDesa.create.form.deskripsi?.trim() !== '' &&
+ stateTelunjukDesa.create.form.link?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stateTelunjukDesa.create.form = {
name: '',
@@ -32,6 +41,21 @@ function CreatePelayananTelunjukDesa() {
};
const handleSubmit = async () => {
+ if (!stateTelunjukDesa.create.form.name?.trim()) {
+ toast.error('Nama pelayanan wajib diisi');
+ return;
+ }
+
+ if (!stateTelunjukDesa.create.form.deskripsi?.trim()) {
+ toast.error('Judul link wajib diisi');
+ return;
+ }
+
+ if (!stateTelunjukDesa.create.form.link?.trim()) {
+ toast.error('Link wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await stateTelunjukDesa.create.create();
@@ -118,8 +142,11 @@ function CreatePelayananTelunjukDesa() {
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, #eeeeee)`
+ : `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)/desa/penghargaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
index 08084881..7545a147 100644
--- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
@@ -33,6 +33,23 @@ function EditPenghargaan() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.juara?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ (file !== null || originalData.imageId !== '') // Either a new file is selected or an existing image exists
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
juara: "",
@@ -100,6 +117,26 @@ function EditPenghargaan() {
// Submit
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama penghargaan wajib diisi');
+ return;
+ }
+
+ if (!formData.juara?.trim()) {
+ toast.error('Juara wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (!file && !originalData.imageId) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Sync ke global state saat submit
@@ -281,8 +318,11 @@ function EditPenghargaan() {
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, #eeeeee)`
+ : `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)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
index d0d9bd7b..00748810 100644
--- a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
@@ -30,6 +30,23 @@ function CreatePenghargaan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ statePenghargaan.create.form.name?.trim() !== '' &&
+ statePenghargaan.create.form.juara?.trim() !== '' &&
+ !isHtmlEmpty(statePenghargaan.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
statePenghargaan.create.form = {
name: '',
@@ -42,6 +59,26 @@ function CreatePenghargaan() {
};
const handleSubmit = async () => {
+ if (!statePenghargaan.create.form.name?.trim()) {
+ toast.error('Nama penghargaan wajib diisi');
+ return;
+ }
+
+ if (!statePenghargaan.create.form.juara?.trim()) {
+ toast.error('Juara wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(statePenghargaan.create.form.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (!file) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!file) {
@@ -201,8 +238,11 @@ function CreatePenghargaan() {
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, #eeeeee)`
+ : `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)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
index a9b8fa2a..c4523f32 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
@@ -27,6 +27,13 @@ function EditKategoriPengumuman() {
const [formData, setFormData] = useState({ name: '' });
const [originalData, setOriginalData] = useState({ name: '' });
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== ''
+ );
+ };
+
// Load data awal sekali aja
useEffect(() => {
const loadKategori = async () => {
@@ -56,6 +63,11 @@ function EditKategoriPengumuman() {
};
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama kategori pengumuman wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Update global state hanya di sini
@@ -134,8 +146,11 @@ function EditKategoriPengumuman() {
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, #eeeeee)`
+ : `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)/desa/pengumuman/kategori-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
index aeb5815e..c3eac62d 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
@@ -22,6 +22,13 @@ function CreateKategoriPengumuman() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.name?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
createState.create.form = {
name: '',
@@ -29,6 +36,11 @@ function CreateKategoriPengumuman() {
};
const handleSubmit = async () => {
+ if (!createState.create.form.name?.trim()) {
+ toast.error('Nama kategori pengumuman wajib diisi');
+ return;
+ }
+
try {
await createState.create.create();
resetForm();
@@ -92,8 +104,11 @@ function CreateKategoriPengumuman() {
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, #eeeeee)`
+ : `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)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
index 511fcaeb..5e41d683 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
@@ -36,6 +36,23 @@ function EditPengumuman() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ formData.deskripsi?.trim() !== '' &&
+ formData.categoryPengumumanId !== '' &&
+ !isHtmlEmpty(formData.content)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
@@ -81,6 +98,26 @@ function EditPengumuman() {
};
const handleSubmit = async () => {
+ if (!formData.judul?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!formData.deskripsi?.trim()) {
+ toast.error('Deskripsi singkat wajib diisi');
+ return;
+ }
+
+ if (!formData.categoryPengumumanId) {
+ toast.error('Kategori wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.content)) {
+ toast.error('Konten lengkap wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// update global state hanya sekali pas submit
@@ -197,8 +234,11 @@ function EditPengumuman() {
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, #eeeeee)`
+ : `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)/desa/pengumuman/list-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
index 45b400a3..52eaf9d3 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
@@ -27,11 +27,48 @@ function CreatePengumuman() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ pengumumanState.pengumuman.create.form.judul?.trim() !== '' &&
+ pengumumanState.pengumuman.create.form.categoryPengumumanId !== '' &&
+ pengumumanState.pengumuman.create.form.deskripsi?.trim() !== '' &&
+ !isHtmlEmpty(pengumumanState.pengumuman.create.form.content)
+ );
+ };
+
useShallowEffect(() => {
pengumumanState.category.findMany.load();
}, []);
const handleSubmit = async () => {
+ if (!pengumumanState.pengumuman.create.form.judul?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!pengumumanState.pengumuman.create.form.categoryPengumumanId) {
+ toast.error('Kategori wajib dipilih');
+ return;
+ }
+
+ if (!pengumumanState.pengumuman.create.form.deskripsi?.trim()) {
+ toast.error('Deskripsi singkat wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(pengumumanState.pengumuman.create.form.content)) {
+ toast.error('Konten lengkap wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await pengumumanState.pengumuman.create.create();
@@ -150,8 +187,11 @@ function CreatePengumuman() {
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, #eeeeee)`
+ : `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)/desa/potensi/kategori-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
index e7831efc..66809e6c 100644
--- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
@@ -33,6 +33,13 @@ function EditKategoriPotensi() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== ''
+ );
+ };
+
// Load data dari backend -> isi ke formData lokal
useEffect(() => {
const loadKategori = async () => {
@@ -73,6 +80,11 @@ function EditKategoriPotensi() {
};
const handleSubmit = async () => {
+ if (!formData.nama?.trim()) {
+ toast.error('Nama kategori potensi wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Update global state hanya pas submit
@@ -141,8 +153,11 @@ function EditKategoriPotensi() {
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, #eeeeee)`
+ : `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)/desa/potensi/kategori-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
index 53a66933..72659e74 100644
--- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
@@ -21,6 +21,13 @@ function CreateKategoriPotensi() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.nama?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
createState.create.form = {
nama: '',
@@ -28,6 +35,11 @@ function CreateKategoriPotensi() {
};
const handleSubmit = async () => {
+ if (!createState.create.form.nama?.trim()) {
+ alert('Nama kategori potensi wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await createState.create.create();
@@ -91,8 +103,11 @@ function CreateKategoriPotensi() {
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, #eeeeee)`
+ : `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)/desa/potensi/list-potensi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
index 2bf0a23c..600fc2ef 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
@@ -42,6 +42,24 @@ function EditPotensi() {
});
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.kategoriId !== '' &&
+ (file !== null || originalData.imageId !== '') && // Either a new file is selected or an existing image exists
+ !isHtmlEmpty(formData.content)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
@@ -109,6 +127,31 @@ function EditPotensi() {
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi singkat wajib diisi');
+ return;
+ }
+
+ if (!formData.kategoriId) {
+ toast.error('Kategori wajib dipilih');
+ return;
+ }
+
+ if (!file && !originalData.imageId) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.content)) {
+ toast.error('Konten lengkap wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
let imageId = formData.imageId;
@@ -341,8 +384,11 @@ function EditPotensi() {
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, #eeeeee)`
+ : `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)/desa/potensi/list-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
index 3ae6dac8..35fe407a 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
@@ -8,6 +8,7 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
+import DOMPurify from 'dompurify';
export default function DetailPotensi() {
const router = useRouter();
@@ -77,7 +78,17 @@ export default function DetailPotensi() {
Deskripsi
-
+
@@ -102,7 +113,12 @@ export default function DetailPotensi() {
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
index ed642b8f..3fc3fa66 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
@@ -32,11 +32,54 @@ function CreatePotensi() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ potensiState.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(potensiState.create.form.deskripsi) &&
+ potensiState.create.form.kategoriId !== '' &&
+ file !== null &&
+ !isHtmlEmpty(potensiState.create.form.content)
+ );
+ };
+
useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load();
}, []);
const handleSubmit = async () => {
+ if (!potensiState.create.form.name?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(potensiState.create.form.deskripsi)) {
+ toast.error('Deskripsi singkat wajib diisi');
+ return;
+ }
+
+ if (!potensiState.create.form.kategoriId) {
+ toast.error('Kategori wajib dipilih');
+ return;
+ }
+
+ if (!file) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(potensiState.create.form.content)) {
+ toast.error('Konten lengkap wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
@@ -266,8 +309,11 @@ function CreatePotensi() {
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, #eeeeee)`
+ : `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)/desa/potensi/list-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
index f2cb3d15..5ed853da 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
@@ -27,6 +27,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import potensiDesaState from '../../../_state/desa/potensi';
import { useDebouncedValue } from '@mantine/hooks';
+import DOMPurify from 'dompurify';
function Potensi() {
const [search, setSearch] = useState("");
@@ -137,7 +138,12 @@ function ListPotensi({ search }: { search: string }) {
fz="sm"
lh={1.5}
lineClamp={2}
- dangerouslySetInnerHTML={{ __html: item.deskripsi }}
+ dangerouslySetInnerHTML={{
+ __html: DOMPurify.sanitize(item.deskripsi, {
+ ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
+ ALLOWED_ATTR: []
+ })
+ }}
style={{ wordBreak: 'break-word' }}
/>
@@ -199,7 +205,12 @@ function ListPotensi({ search }: { search: string }) {
diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx
index 9544947b..955ce10d 100644
--- a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx
@@ -83,6 +83,21 @@ function Page() {
};
}, [params?.id, router]);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// 🔁 Reset form
const handleResetForm = () => {
setFormData(originalData);
@@ -95,6 +110,11 @@ function Page() {
toast.error('Judul wajib diisi');
return;
}
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
setIsSubmitting(true);
try {
@@ -224,14 +244,16 @@ function Page() {
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, #eeeeee)`
+ : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
- loading={isSubmitting}
>
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx
index 82d56817..f5dfff4d 100644
--- a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx
@@ -34,6 +34,21 @@ function Page() {
images: [] as Array<{ label: string; imageId: string }>
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data
useEffect(() => {
const loadData = async () => {
@@ -122,6 +137,12 @@ function Page() {
toast.error("Judul wajib diisi");
return;
}
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error("Deskripsi wajib diisi");
+ return;
+ }
+
try {
setIsSubmitting(true);
const uploadedImages = [];
@@ -315,8 +336,11 @@ function Page() {
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, #eeeeee)`
+ : `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)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx
index f0b084eb..10f3105e 100644
--- a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx
@@ -99,6 +99,21 @@ function Page() {
toast.info('Form dikembalikan ke data awal');
};
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// 💾 Submit Handler
const handleSubmit = async () => {
// Validation
@@ -106,6 +121,11 @@ function Page() {
toast.error('Judul wajib diisi');
return;
}
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
setIsSubmitting(true);
@@ -260,8 +280,11 @@ function Page() {
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, #eeeeee)`
+ : `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)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx
index 62a45c04..7a743755 100644
--- a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx
@@ -81,6 +81,21 @@ function Page() {
};
}, [params?.id, router]);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.visi) &&
+ !isHtmlEmpty(formData.misi)
+ );
+ };
+
// 🔄 Reset Form
const handleResetForm = () => {
setFormData(originalData);
@@ -89,11 +104,16 @@ function Page() {
// 💾 Submit
const handleSubmit = async () => {
- if (!formData.visi.trim()) {
+ if (isHtmlEmpty(formData.visi)) {
toast.error('Visi wajib diisi');
return;
}
+ if (isHtmlEmpty(formData.misi)) {
+ toast.error('Misi wajib diisi');
+ return;
+ }
+
setIsSubmitting(true);
try {
const originalState = stateProfileDesa.visiMisiDesa;
@@ -227,14 +247,16 @@ function Page() {
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, #eeeeee)`
+ : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
- loading={isSubmitting}
>
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx
index d3b1cb21..e79103d6 100644
--- a/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx
@@ -95,7 +95,7 @@ function Page() {
fz={{ base: 'md', md: 'lg' }}
lh={{ base: 1.4, md: 1.4 }}
>
- I.B. Surya Prabhawa Manuaba, S.H., M.H.
+ {perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
index 3cd8c1f6..b8d1e86e 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
@@ -51,6 +51,17 @@ function EditAPBDesa() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(true);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.tahun?.trim() !== '' &&
+ Number(formData.tahun) > 0 &&
+ formData.pendapatanIds.length > 0 &&
+ formData.belanjaIds.length > 0 &&
+ formData.pembiayaanIds.length > 0
+ );
+ };
+
// ==================== LOAD DATA ====================
useEffect(() => {
const loadAPBdesa = async () => {
@@ -213,8 +224,11 @@ function EditAPBDesa() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
index 6ab98928..cc237b70 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
@@ -27,6 +27,17 @@ function CreateAPBDesa() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ apbDesaState.create.form.tahun !== null &&
+ apbDesaState.create.form.tahun > 0 &&
+ apbDesaState.create.form.pendapatanIds.length > 0 &&
+ apbDesaState.create.form.belanjaIds.length > 0 &&
+ apbDesaState.create.form.pembiayaanIds.length > 0
+ );
+ };
+
const resetForm = () => {
apbDesaState.create.form = {
tahun: 0,
@@ -121,8 +132,11 @@ function CreateAPBDesa() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
index 96bb9ebd..45e5937a 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
@@ -35,6 +35,15 @@ function EditBelanja() {
value: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.value !== '' &&
+ Number(formData.value) > 0
+ );
+ };
+
// format angka ke rupiah
const formatRupiah = (value: number | string) => {
const number =
@@ -172,8 +181,11 @@ function EditBelanja() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
index 013ffc24..d5fede9b 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
@@ -24,6 +24,15 @@ function CreateBelanja() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ belanjaState.create.form.name?.trim() !== '' &&
+ belanjaState.create.form.value !== null &&
+ belanjaState.create.form.value > 0
+ );
+ };
+
const formatRupiah = (value: number | string) => {
const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
@@ -126,8 +135,11 @@ function CreateBelanja() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
index b0b0ed38..378a34fd 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
@@ -34,6 +34,15 @@ function EditPembiayaan() {
value: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.value !== '' &&
+ Number(formData.value) > 0
+ );
+ };
+
const formatRupiah = (value: number | string) => {
const number =
typeof value === 'number'
@@ -169,8 +178,11 @@ function EditPembiayaan() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
index 56b71894..63897a2f 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
@@ -23,6 +23,15 @@ function CreatePembiayaan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ pembiayaanState.create.form.name?.trim() !== '' &&
+ pembiayaanState.create.form.value !== null &&
+ pembiayaanState.create.form.value > 0
+ );
+ };
+
const formatRupiah = (value: number | string) => {
const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
@@ -127,8 +136,11 @@ function CreatePembiayaan() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
index 5b68ff7e..9bddfead 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
@@ -34,6 +34,15 @@ function EditPendapatan() {
value: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.value !== '' &&
+ Number(formData.value) > 0
+ );
+ };
+
// helper format
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number'
@@ -176,8 +185,11 @@ function EditPendapatan() {
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, #eeeeee)`
+ : `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)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
index 0c3ee514..ca32a516 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
@@ -22,6 +22,15 @@ function CreatePendapatan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ pendapatanState.create.form.name?.trim() !== '' &&
+ pendapatanState.create.form.value !== null &&
+ pendapatanState.create.form.value > 0
+ );
+ };
+
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
@@ -122,8 +131,11 @@ function CreatePendapatan() {
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, #eeeeee)`
+ : `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)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx
index ef14160e..aebb8c18 100644
--- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx
@@ -56,6 +56,14 @@ export default function EditPegawaiBumDes() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.namaLengkap?.trim() !== '' &&
+ formData.posisiId?.trim() !== ''
+ );
+ };
+
// Format date for
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
@@ -325,8 +333,11 @@ export default function EditPegawaiBumDes() {
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, #eeeeee)`
+ : `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)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx
index 5a8d1d1d..4999449a 100644
--- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx
@@ -23,6 +23,15 @@ function CreatePegawaiBumDes() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateOrganisasi.create.form.namaLengkap?.trim() !== '' &&
+ stateOrganisasi.create.form.posisiId?.trim() !== '' &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateOrganisasi.create.form = {
namaLengkap: "",
@@ -284,8 +293,11 @@ function CreatePegawaiBumDes() {
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, #eeeeee)`
+ : `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)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx
index cb9f03ff..32496484 100644
--- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx
@@ -31,6 +31,23 @@ function EditPosisiOrganisasiBumDes() {
hierarki: 0,
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.hierarki !== null &&
+ formData.hierarki >= 0
+ );
+ };
+
// Fungsi generik untuk update formData
const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -173,8 +190,11 @@ function EditPosisiOrganisasiBumDes() {
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, #eeeeee)`
+ : `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)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx
index 84aaabcf..32b88dd3 100644
--- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx
@@ -15,6 +15,23 @@ function CreatePosisiOrganisasiBumDes() {
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateOrganisasi.create.form.nama?.trim() !== '' &&
+ !isHtmlEmpty(stateOrganisasi.create.form.deskripsi) &&
+ stateOrganisasi.create.form.hierarki !== null &&
+ stateOrganisasi.create.form.hierarki >= 0
+ );
+ };
+
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
@@ -115,8 +132,11 @@ function CreatePosisiOrganisasiBumDes() {
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, #eeeeee)`
+ : `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)/ekonomi/demografi-pekerjaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
index 4ad28f5e..b5f5f37b 100644
--- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
@@ -41,6 +41,17 @@ export default function EditDemografiPekerjaan() {
perempuan: 0,
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.pekerjaan?.trim() !== '' &&
+ formData.lakiLaki !== null &&
+ formData.lakiLaki >= 0 &&
+ formData.perempuan !== null &&
+ formData.perempuan >= 0
+ );
+ };
+
// ✅ Load data hanya sekali di awal (tidak reset form)
useEffect(() => {
if (!id) return;
@@ -186,8 +197,11 @@ export default function EditDemografiPekerjaan() {
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, #eeeeee)`
+ : `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)/ekonomi/demografi-pekerjaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
index d8284367..ed5c84f1 100644
--- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
@@ -26,6 +26,17 @@ function CreateDemografiPekerjaan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateDemografi.create.form.pekerjaan?.trim() !== '' &&
+ stateDemografi.create.form.lakiLaki !== null &&
+ stateDemografi.create.form.lakiLaki >= 0 &&
+ stateDemografi.create.form.perempuan !== null &&
+ stateDemografi.create.form.perempuan >= 0
+ );
+ };
+
const resetForm = () => {
stateDemografi.create.form = {
pekerjaan: '',
@@ -128,8 +139,11 @@ function CreateDemografiPekerjaan() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx
index 6dd1ff92..4637dfeb 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx
@@ -37,6 +37,16 @@ function EditJumlahPendudukMiskin() {
totalPoorPopulation: 0,
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.year !== null &&
+ formData.year > 0 &&
+ formData.totalPoorPopulation !== null &&
+ formData.totalPoorPopulation >= 0
+ );
+ };
+
// 🔹 Load data awal dari backend
useEffect(() => {
if (!id) return;
@@ -160,8 +170,11 @@ function EditJumlahPendudukMiskin() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-penduduk-miskin/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
index b26253a1..a911ceef 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
@@ -16,6 +16,16 @@ export default function CreateJumlahPendudukMiskin() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateJPM.create.form.year !== null &&
+ stateJPM.create.form.year > 0 &&
+ stateJPM.create.form.totalPoorPopulation !== null &&
+ stateJPM.create.form.totalPoorPopulation >= 0
+ );
+ };
+
const resetForm = () => {
stateJPM.create.form = {
year: new Date().getFullYear(),
@@ -105,8 +115,11 @@ export default function CreateJumlahPendudukMiskin() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
index bb0a39e6..62e66df3 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
@@ -33,6 +33,17 @@ function EditGrafikBerdasarkanPendidikan() {
S1: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.SD?.trim() !== '' &&
+ formData.SMP?.trim() !== '' &&
+ formData.SMA?.trim() !== '' &&
+ formData.D3?.trim() !== '' &&
+ formData.S1?.trim() !== ''
+ );
+ };
+
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
@@ -174,8 +185,11 @@ function EditGrafikBerdasarkanPendidikan() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
index 73d89a1a..dfa0f954 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
@@ -16,6 +16,17 @@ function CreateGrafikBerdasarkanPendidikan() {
const [donutData, setDonutData] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stategrafik.create.form.SD?.trim() !== '' &&
+ stategrafik.create.form.SMP?.trim() !== '' &&
+ stategrafik.create.form.SMA?.trim() !== '' &&
+ stategrafik.create.form.D3?.trim() !== '' &&
+ stategrafik.create.form.S1?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stategrafik.create.form = {
...stategrafik.create.form,
@@ -127,8 +138,11 @@ function CreateGrafikBerdasarkanPendidikan() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
index 51244c13..8c87f29b 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
@@ -43,6 +43,16 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
usia46_keatas: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.usia18_25?.trim() !== '' &&
+ formData.usia26_35?.trim() !== '' &&
+ formData.usia36_45?.trim() !== '' &&
+ formData.usia46_keatas?.trim() !== ''
+ );
+ };
+
// load data dari global state -> masukin ke local state
useEffect(() => {
if (id) {
@@ -179,8 +189,11 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
index f31cf437..26d51682 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
@@ -17,6 +17,16 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [donutData, setDonutData] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stategrafik.create.form.usia18_25?.trim() !== '' &&
+ stategrafik.create.form.usia26_35?.trim() !== '' &&
+ stategrafik.create.form.usia36_45?.trim() !== '' &&
+ stategrafik.create.form.usia46_keatas?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stategrafik.create.form = {
...stategrafik.create.form,
@@ -120,8 +130,11 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx
index 4a3bf4f3..5ccf3989 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx
@@ -53,6 +53,19 @@ function EditDetailDataPengangguran() {
percentageChange: 0,
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.month?.trim() !== '' &&
+ formData.year !== null &&
+ formData.year > 0 &&
+ formData.educatedUnemployment !== null &&
+ formData.educatedUnemployment >= 0 &&
+ formData.uneducatedUnemployment !== null &&
+ formData.uneducatedUnemployment >= 0
+ );
+ };
+
// --- hitung total + persentase perubahan
const calculateTotalAndChange = useCallback(
async (data: typeof formData) => {
@@ -255,8 +268,11 @@ function EditDetailDataPengangguran() {
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, #eeeeee)`
+ : `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)/ekonomi/jumlah-pengangguran/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
index e7583359..3cd144f6 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
@@ -27,6 +27,19 @@ function CreateJumlahPengangguran() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateDetail.create.form.month?.trim() !== '' &&
+ stateDetail.create.form.year !== null &&
+ stateDetail.create.form.year > 0 &&
+ stateDetail.create.form.educatedUnemployment !== null &&
+ stateDetail.create.form.educatedUnemployment >= 0 &&
+ stateDetail.create.form.uneducatedUnemployment !== null &&
+ stateDetail.create.form.uneducatedUnemployment >= 0
+ );
+ };
+
const monthOptions = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
@@ -204,8 +217,11 @@ function CreateJumlahPengangguran() {
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, #eeeeee)`
+ : `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)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
index 3fdad936..aee1b8c0 100644
--- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
@@ -48,6 +48,27 @@ function EditLowonganKerja() {
notelp: '',
})
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.posisi?.trim() !== '' &&
+ formData.namaPerusahaan?.trim() !== '' &&
+ formData.notelp?.trim() !== '' &&
+ formData.lokasi?.trim() !== '' &&
+ formData.tipePekerjaan?.trim() !== '' &&
+ formData.gaji?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ !isHtmlEmpty(formData.kualifikasi)
+ );
+ };
+
// load data sekali aja ketika mount / id berubah
useEffect(() => {
const loadLowongan = async () => {
@@ -229,8 +250,11 @@ function EditLowonganKerja() {
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, #eeeeee)`
+ : `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)/ekonomi/lowongan-kerja-lokal/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
index dfbb955a..055c9ddd 100644
--- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
@@ -24,6 +24,27 @@ function CreateLowonganKerja() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ lowonganState.create.form.posisi?.trim() !== '' &&
+ lowonganState.create.form.namaPerusahaan?.trim() !== '' &&
+ lowonganState.create.form.notelp?.trim() !== '' &&
+ lowonganState.create.form.lokasi?.trim() !== '' &&
+ lowonganState.create.form.tipePekerjaan?.trim() !== '' &&
+ lowonganState.create.form.gaji?.trim() !== '' &&
+ !isHtmlEmpty(lowonganState.create.form.deskripsi) &&
+ !isHtmlEmpty(lowonganState.create.form.kualifikasi)
+ );
+ };
+
const resetForm = () => {
lowonganState.create.form = {
posisi: '',
@@ -175,8 +196,11 @@ function CreateLowonganKerja() {
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, #eeeeee)`
+ : `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)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
index 1b755f90..c180ad4a 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
@@ -28,6 +28,11 @@ function EditKategoriProduk() {
const [formData, setFormData] = useState({ nama: '' });
const [originalData, setOriginalData] = useState({ nama: '' });
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.nama?.trim() !== '';
+ };
+
useEffect(() => {
const loadKategoriProduk = async () => {
if (!id) return;
@@ -146,8 +151,11 @@ function EditKategoriProduk() {
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, #eeeeee)`
+ : `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)/ekonomi/pasar-desa/kategori-produk/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
index 51ee8a3d..5873d9ae 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
@@ -23,6 +23,11 @@ function CreateKategoriProduk() {
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return statePasar.create.form.nama?.trim() !== '';
+ };
+
useEffect(() => {
statePasar.findMany.load();
}, []);
@@ -101,8 +106,11 @@ function CreateKategoriProduk() {
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, #eeeeee)`
+ : `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)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
index f656a13f..0fc729ac 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
@@ -68,6 +68,23 @@ function EditPasarDesa() {
deskripsi: ''
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.harga !== null &&
+ formData.harga > 0 &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// load data awal
useEffect(() => {
pasarState.kategoriProduk.findManyAll.load();
@@ -352,8 +369,11 @@ function EditPasarDesa() {
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, #eeeeee)`
+ : `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)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
index 0ece1759..0fc7fc03 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
@@ -32,6 +32,24 @@ export default function CreatePasarDesa() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ statePasar.pasarDesa.create.form.nama?.trim() !== '' &&
+ statePasar.pasarDesa.create.form.harga !== null &&
+ statePasar.pasarDesa.create.form.harga > 0 &&
+ !isHtmlEmpty(statePasar.pasarDesa.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
useEffect(() => {
statePasar.kategoriProduk.findManyAll.load();
}, []);
@@ -265,8 +283,11 @@ export default function CreatePasarDesa() {
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, #eeeeee)`
+ : `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)/ekonomi/program-kemiskinan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
index f3cbf5bb..a5f2a416 100644
--- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
@@ -53,6 +53,24 @@ function EditProgramKemiskinan() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState(initialForm);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.icon?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.statistik.jumlah?.trim() !== '' &&
+ formData.statistik.tahun?.trim() !== ''
+ );
+ };
+
// Load data 1x dari global state → isi local state
useEffect(() => {
if (!id) return;
@@ -235,8 +253,11 @@ function EditProgramKemiskinan() {
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, #eeeeee)`
+ : `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)/ekonomi/program-kemiskinan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
index 674bb6a1..4d664ffb 100644
--- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
@@ -29,6 +29,25 @@ function CreateProgramKemiskinan() {
const [lineChart, setLineChart] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ programState.create.form.nama?.trim() !== '' &&
+ programState.create.form.icon?.trim() !== '' &&
+ !isHtmlEmpty(programState.create.form.deskripsi) &&
+ programState.create.form.statistik.jumlah?.trim() !== '' &&
+ programState.create.form.statistik.tahun?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
programState.create.form = {
nama: '',
@@ -172,8 +191,11 @@ function CreateProgramKemiskinan() {
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, #eeeeee)`
+ : `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)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
index 592e7a35..da00ccba 100644
--- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
@@ -41,6 +41,23 @@ function EditSektorUnggulanDesa() {
value: 0,
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.description) &&
+ formData.value !== null &&
+ formData.value >= 0
+ );
+ };
+
// Load data saat komponen mount
useEffect(() => {
if (id) {
@@ -168,8 +185,11 @@ function EditSektorUnggulanDesa() {
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, #eeeeee)`
+ : `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)/ekonomi/sektor-unggulan-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
index f5beda9c..edd2c503 100644
--- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
@@ -27,6 +27,23 @@ function CreateSektorUnggulanDesa() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateGrafik.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(stateGrafik.create.form.description) &&
+ stateGrafik.create.form.value !== null &&
+ stateGrafik.create.form.value >= 0
+ );
+ };
+
const resetForm = () => {
stateGrafik.create.form = {
name: '',
@@ -132,8 +149,11 @@ function CreateSektorUnggulanDesa() {
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, #eeeeee)`
+ : `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)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
index 350cbd38..05a941b5 100644
--- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
@@ -44,6 +44,21 @@ function EditDigitalSmartVillage() {
imageUrl: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
@@ -248,8 +263,11 @@ function EditDigitalSmartVillage() {
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, #eeeeee)`
+ : `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)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
index 56426951..051861a2 100644
--- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
@@ -30,6 +30,22 @@ export default function CreateDesaDigital() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateDesaDigital.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(stateDesaDigital.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateDesaDigital.create.form = {
name: '',
@@ -227,8 +243,11 @@ export default function CreateDesaDigital() {
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, #eeeeee)`
+ : `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)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx
index e8f2d36c..241c96fa 100644
--- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx
@@ -44,6 +44,21 @@ function EditInfoTeknologiTepatGuna() {
imageUrl: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data pertama kali
useEffect(() => {
const id = params?.id as string;
@@ -260,8 +275,11 @@ function EditInfoTeknologiTepatGuna() {
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, #eeeeee)`
+ : `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)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
index 035d6c0c..f0c82b63 100644
--- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
@@ -30,6 +30,22 @@ function CreateInfoTeknologiTepatGuna() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateInfoTekno.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(stateInfoTekno.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateInfoTekno.create.form = {
name: '',
@@ -202,8 +218,11 @@ function CreateInfoTeknologiTepatGuna() {
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, #eeeeee)`
+ : `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)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
index d07058ef..48134a46 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
@@ -44,6 +44,23 @@ function EditKolaborasiInovasi() {
kolaborator: "",
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.slug?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.kolaborator?.trim() !== ''
+ );
+ };
+
// Load data awal dari server
useEffect(() => {
const loadKolaborasi = async () => {
@@ -199,8 +216,11 @@ 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, #eeeeee)`
+ : `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)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
index c400f193..06179a19 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
@@ -16,6 +16,22 @@ function CreateProgramKreatifDesa() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.name?.trim() !== '' &&
+ stateCreate.create.form.slug?.trim() !== '' &&
+ !isHtmlEmpty(stateCreate.create.form.deskripsi)
+ );
+ };
+
const resetForm = () => {
stateCreate.create.form = {
name: "",
@@ -135,8 +151,11 @@ function CreateProgramKreatifDesa() {
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, #eeeeee)`
+ : `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)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
index a14a850b..9e592d72 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
@@ -51,6 +51,11 @@ function EditMitraKolaborasi() {
imageUrl: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.name?.trim() !== '';
+ };
+
// Load data ke state lokal sekali saja
useEffect(() => {
const loadData = async () => {
@@ -263,8 +268,11 @@ function EditMitraKolaborasi() {
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, #eeeeee)`
+ : `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)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
index c6c77ad2..ba4c52aa 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
@@ -29,6 +29,14 @@ function CreateMitraKolaborasi() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ state.create.form.name?.trim() !== '' &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
state.create.form = {
name: '',
@@ -181,8 +189,11 @@ function CreateMitraKolaborasi() {
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, #eeeeee)`
+ : `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)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
index baaba7c0..ef76991f 100644
--- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
@@ -51,6 +51,23 @@ function EditProgramKreatifDesa() {
const [isDataChanged, setIsDataChanged] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.slug?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.icon?.trim() !== ''
+ );
+ };
+
// Load data hanya sekali berdasarkan params.id
useEffect(() => {
const loadProgramKreatif = async () => {
@@ -236,8 +253,11 @@ function EditProgramKreatifDesa() {
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, #eeeeee)`
+ : `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)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
index ee861f65..a70ce546 100644
--- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
@@ -25,6 +25,23 @@ function CreateProgramKreatifDesa() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.name?.trim() !== '' &&
+ stateCreate.create.form.icon?.trim() !== '' &&
+ stateCreate.create.form.slug?.trim() !== '' &&
+ !isHtmlEmpty(stateCreate.create.form.deskripsi)
+ );
+ };
+
const resetForm = () => {
stateCreate.create.form = {
name: "",
@@ -127,8 +144,11 @@ function CreateProgramKreatifDesa() {
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, #eeeeee)`
+ : `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)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
index fdf71259..b23b011a 100644
--- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
@@ -53,6 +53,21 @@ function EditKeamananLingkungan() {
imageUrl: "",
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data sekali pas mount
useEffect(() => {
const loadData = async () => {
@@ -294,8 +309,11 @@ function EditKeamananLingkungan() {
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, #eeeeee)`
+ : `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)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
index f1d148a6..6b977006 100644
--- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
@@ -35,6 +35,22 @@ function CreateKeamananLingkungan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false)
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ keamananState.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(keamananState.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
keamananState.create.form = {
name: '',
@@ -237,8 +253,11 @@ function CreateKeamananLingkungan() {
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, #eeeeee)`
+ : `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)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
index 7bff7bad..3d4ff3b5 100644
--- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
@@ -39,6 +39,15 @@ function EditKontakItem() {
icon: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.nomorTelepon?.trim() !== '' &&
+ formData.icon?.trim() !== ''
+ );
+ };
+
// Load data sekali dari global state
useEffect(() => {
const loadKontakDarurat = async () => {
@@ -170,8 +179,11 @@ function EditKontakItem() {
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, #eeeeee)`
+ : `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)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx
index c6789784..ea0ac7fe 100644
--- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx
@@ -23,6 +23,16 @@ function CreateKontakItem() {
const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ kontakState.create.form.nama?.trim() !== '' &&
+ kontakState.create.form.nomorTelepon?.trim() !== '' &&
+ kontakState.create.form.icon?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
kontakState.create.form = {
nama: '',
@@ -115,8 +125,11 @@ function CreateKontakItem() {
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, #eeeeee)`
+ : `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)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx
index 80a8c6f1..de6ac436 100644
--- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx
@@ -46,6 +46,14 @@ export default function EditKontakDaruratKeamanan() {
kategoriId: [],
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.kategoriId.length > 0
+ );
+ };
+
// 🔁 Load data saat ID berubah
useEffect(() => {
const loadInitialData = async () => {
@@ -213,8 +221,11 @@ export default function EditKontakDaruratKeamanan() {
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, #eeeeee)`
+ : `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)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx
index 92c0a424..bd8e0b69 100644
--- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx
@@ -26,6 +26,14 @@ function CreateKontakDaruratKeamanan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ kontakState.create.form.nama?.trim() !== '' &&
+ kontakState.create.form.icon?.trim() !== ''
+ );
+ };
+
useShallowEffect(() => {
kontakDarurat.kontakDaruratItem.findMany.load();
}, []);
@@ -127,8 +135,11 @@ function CreateKontakDaruratKeamanan() {
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, #eeeeee)`
+ : `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)/keamanan/laporan-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx
index a079c8aa..f9f940a7 100644
--- a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx
@@ -55,6 +55,25 @@ function EditLaporanPublik() {
kronologi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ formData.lokasi?.trim() !== '' &&
+ formData.tanggalWaktu?.trim() !== '' &&
+ formData.status?.trim() !== '' &&
+ !isHtmlEmpty(formData.kronologi) &&
+ !isHtmlEmpty(formData.penanganan)
+ );
+ };
+
useEffect(() => {
const loadLaporanPublik = async () => {
const id = params?.id as string;
@@ -223,8 +242,11 @@ function EditLaporanPublik() {
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, #eeeeee)`
+ : `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)/keamanan/laporan-publik/create/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx
index 4665545d..d086b10f 100644
--- a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx
@@ -26,6 +26,16 @@ function CreateLaporanPublik() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateLaporan.create.form.judul?.trim() !== '' &&
+ stateLaporan.create.form.lokasi?.trim() !== '' &&
+ stateLaporan.create.form.tanggalWaktu?.trim() !== '' &&
+ stateLaporan.create.form.kronologi?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stateLaporan.create.form = {
judul: '',
@@ -123,8 +133,11 @@ function CreateLaporanPublik() {
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, #eeeeee)`
+ : `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)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx
index 9943477a..1a63549c 100644
--- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx
@@ -41,6 +41,23 @@ function EditPencegahanKriminalitas() {
linkVideo: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsiSingkat) &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ convertYoutubeUrlToEmbed(formData.linkVideo) !== ''
+ );
+ };
+
// load data hanya sekali pas id berubah
useEffect(() => {
const loadKriminalitas = async () => {
@@ -220,8 +237,11 @@ function EditPencegahanKriminalitas() {
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, #eeeeee)`
+ : `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)/keamanan/pencegahan-kriminalitas/create/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx
index ab5646c8..9e12f6d4 100644
--- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx
@@ -28,6 +28,23 @@ function CreatePencegahanKriminalitas() {
const embedLink = convertYoutubeUrlToEmbed(link);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ kriminalitasState.create.form.judul?.trim() !== '' &&
+ !isHtmlEmpty(kriminalitasState.create.form.deskripsiSingkat) &&
+ !isHtmlEmpty(kriminalitasState.create.form.deskripsi) &&
+ embedLink !== ''
+ );
+ };
+
const resetForm = () => {
kriminalitasState.create.form = {
judul: "",
@@ -165,8 +182,11 @@ function CreatePencegahanKriminalitas() {
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, #eeeeee)`
+ : `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)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx
index 1c2b79d4..29f2a4fd 100644
--- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx
@@ -67,6 +67,17 @@ function EditPolsekTerdekat() {
layananPolsekId: []
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.jarakKeDesa?.trim() !== '' &&
+ formData.alamat?.trim() !== '' &&
+ formData.nomorTelepon?.trim() !== '' &&
+ formData.layananPolsekId.length > 0
+ );
+ };
+
useEffect(() => {
statePolsekTerdekat.layananPolsek.findManyAll.load();
}, []);
@@ -261,8 +272,11 @@ function EditPolsekTerdekat() {
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, #eeeeee)`
+ : `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)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx
index b3b63080..3f38a0d5 100644
--- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx
@@ -24,6 +24,17 @@ function CreatePolsekTerdekat() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ polsekState.create.form.nama?.trim() !== '' &&
+ polsekState.create.form.jarakKeDesa?.trim() !== '' &&
+ polsekState.create.form.alamat?.trim() !== '' &&
+ polsekState.create.form.nomorTelepon?.trim() !== '' &&
+ polsekState.create.form.layananPolsekId.length > 0
+ );
+ };
+
useEffect(() => {
statePolsekTerdekat.layananPolsek.findManyAll.load();
}, []);
@@ -219,8 +230,11 @@ function CreatePolsekTerdekat() {
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, #eeeeee)`
+ : `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)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx
index 04e64bd2..5eaa948d 100644
--- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx
@@ -33,6 +33,11 @@ function EditLayananPolsek() {
nama: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.nama?.trim() !== '';
+ };
+
useEffect(() => {
const loadLayananPolsek = async () => {
const id = params?.id as string;
@@ -143,8 +148,11 @@ function EditLayananPolsek() {
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, #eeeeee)`
+ : `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)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx
index 378ba46e..67d551f4 100644
--- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx
@@ -22,6 +22,11 @@ function CreateLayananPolsek() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return createState.create.form.nama?.trim() !== '';
+ };
+
const resetForm = () => {
createState.create.form = {
nama: '',
@@ -93,8 +98,11 @@ function CreateLayananPolsek() {
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, #eeeeee)`
+ : `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)/keamanan/tips-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx
index 48de4cf7..0616cddc 100644
--- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx
@@ -52,6 +52,21 @@ function EditTipsKeamanan() {
imageUrl: "",
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data saat pertama kali
useEffect(() => {
const loadData = async () => {
@@ -295,8 +310,11 @@ function EditTipsKeamanan() {
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, #eeeeee)`
+ : `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)/keamanan/tips-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx
index 8ba626d6..675544b2 100644
--- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx
@@ -30,6 +30,22 @@ function CreateKeamananLingkungan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateKeamanan.create.form.judul?.trim() !== '' &&
+ !isHtmlEmpty(stateKeamanan.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateKeamanan.create.form = {
judul: '',
@@ -199,8 +215,11 @@ function CreateKeamananLingkungan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx
index 354169d8..857426cf 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx
@@ -30,6 +30,33 @@ function CreateArtikelKesehatan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateArtikelKesehatan.create.form.title?.trim() !== '' &&
+ stateArtikelKesehatan.create.form.content?.trim() !== '' &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.introduction.content) &&
+ stateArtikelKesehatan.create.form.symptom.title?.trim() !== '' &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.symptom.content) &&
+ stateArtikelKesehatan.create.form.prevention.title?.trim() !== '' &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.prevention.content) &&
+ stateArtikelKesehatan.create.form.firstAid.title?.trim() !== '' &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.firstAid.content) &&
+ stateArtikelKesehatan.create.form.mythVsFact.title?.trim() !== '' &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.mitos) &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.fakta) &&
+ !isHtmlEmpty(stateArtikelKesehatan.create.form.doctorSign.content) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateArtikelKesehatan.create.form = {
title: '',
@@ -65,10 +92,79 @@ function CreateArtikelKesehatan() {
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
+
+ if (!stateArtikelKesehatan.create.form.title?.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
+ }
+
+ if (!stateArtikelKesehatan.create.form.content?.trim()) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.introduction.content)) {
+ toast.error('Pendahuluan wajib diisi');
+ return;
+ }
+
+ if (!stateArtikelKesehatan.create.form.symptom.title?.trim()) {
+ toast.error('Judul gejala wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.symptom.content)) {
+ toast.error('Deskripsi gejala wajib diisi');
+ return;
+ }
+
+ if (!stateArtikelKesehatan.create.form.prevention.title?.trim()) {
+ toast.error('Judul pencegahan wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.prevention.content)) {
+ toast.error('Deskripsi pencegahan wajib diisi');
+ return;
+ }
+
+ if (!stateArtikelKesehatan.create.form.firstAid.title?.trim()) {
+ toast.error('Judul pertolongan pertama wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.firstAid.content)) {
+ toast.error('Deskripsi pertolongan pertama wajib diisi');
+ return;
+ }
+
+ if (!stateArtikelKesehatan.create.form.mythVsFact.title?.trim()) {
+ toast.error('Judul mitos vs fakta wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.mitos)) {
+ toast.error('Deskripsi mitos wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.fakta)) {
+ toast.error('Deskripsi fakta wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateArtikelKesehatan.create.form.doctorSign.content)) {
+ toast.error('Deskripsi kapan harus ke dokter wajib diisi');
+ return;
+ }
+
+ if (!file) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
- if (!file) {
- return toast.warn('Silakan pilih file gambar terlebih dahulu');
- }
+ setIsSubmitting(true);
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -344,8 +440,11 @@ function CreateArtikelKesehatan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx
index 32db4e6b..01e6a6ff 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx
@@ -45,6 +45,28 @@ function EditFasilitasKesehatan() {
const params = useParams<{ id: string }>();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.informasiUmum.fasilitas?.trim() !== '' &&
+ formData.informasiUmum.alamat?.trim() !== '' &&
+ formData.informasiUmum.jamOperasional?.trim() !== '' &&
+ !isHtmlEmpty(formData.layananUnggulan.content) &&
+ formData.dokterdanTenagaMedis.length > 0 &&
+ !isHtmlEmpty(formData.fasilitasPendukung.content) &&
+ !isHtmlEmpty(formData.prosedurPendaftaran.content) &&
+ formData.tarifDanLayanan.length > 0
+ );
+ };
+
const [formData, setFormData] = useState({
name: '',
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
@@ -111,6 +133,52 @@ function EditFasilitasKesehatan() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+
+ if (!formData.name?.trim()) {
+ toast.error('Nama fasilitas kesehatan wajib diisi');
+ return;
+ }
+
+ if (!formData.informasiUmum.fasilitas?.trim()) {
+ toast.error('Fasilitas wajib diisi');
+ return;
+ }
+
+ if (!formData.informasiUmum.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
+ if (!formData.informasiUmum.jamOperasional?.trim()) {
+ toast.error('Jam operasional wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.layananUnggulan.content)) {
+ toast.error('Layanan unggulan wajib diisi');
+ return;
+ }
+
+ if (formData.dokterdanTenagaMedis.length === 0) {
+ toast.error('Dokter dan tenaga medis wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.fasilitasPendukung.content)) {
+ toast.error('Fasilitas pendukung wajib diisi');
+ return;
+ }
+
+ if (formData.tarifDanLayanan.length === 0) {
+ toast.error('Tarif dan layanan wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.prosedurPendaftaran.content)) {
+ toast.error('Prosedur pendaftaran wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
@@ -264,8 +332,11 @@ function EditFasilitasKesehatan() {
{
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateFasilitasKesehatan.create.form.name?.trim() !== '' &&
+ stateFasilitasKesehatan.create.form.informasiUmum.fasilitas?.trim() !== '' &&
+ stateFasilitasKesehatan.create.form.informasiUmum.alamat?.trim() !== '' &&
+ stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional?.trim() !== '' &&
+ !isHtmlEmpty(stateFasilitasKesehatan.create.form.layananUnggulan.content) &&
+ stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.length > 0 &&
+ !isHtmlEmpty(stateFasilitasKesehatan.create.form.fasilitasPendukung.content) &&
+ stateFasilitasKesehatan.create.form.tarifDanLayanan.length > 0 &&
+ !isHtmlEmpty(stateFasilitasKesehatan.create.form.prosedurPendaftaran.content)
+ );
+ };
+
const resetForm = () => {
stateFasilitasKesehatan.create.form = {
name: '',
@@ -50,6 +72,52 @@ function CreateFasilitasKesehatan() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+
+ if (!stateFasilitasKesehatan.create.form.name?.trim()) {
+ toast.error('Nama fasilitas kesehatan wajib diisi');
+ return;
+ }
+
+ if (!stateFasilitasKesehatan.create.form.informasiUmum.fasilitas?.trim()) {
+ toast.error('Fasilitas wajib diisi');
+ return;
+ }
+
+ if (!stateFasilitasKesehatan.create.form.informasiUmum.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
+ if (!stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional?.trim()) {
+ toast.error('Jam operasional wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(stateFasilitasKesehatan.create.form.layananUnggulan.content)) {
+ toast.error('Layanan unggulan wajib diisi');
+ return;
+ }
+
+ if (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.length === 0) {
+ toast.error('Dokter dan tenaga medis wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(stateFasilitasKesehatan.create.form.fasilitasPendukung.content)) {
+ toast.error('Fasilitas pendukung wajib diisi');
+ return;
+ }
+
+ if (stateFasilitasKesehatan.create.form.tarifDanLayanan.length === 0) {
+ toast.error('Layanan wajib dipilih');
+ return;
+ }
+
+ if (isHtmlEmpty(stateFasilitasKesehatan.create.form.prosedurPendaftaran.content)) {
+ toast.error('Prosedur pendaftaran wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await stateFasilitasKesehatan.create.submit();
@@ -214,8 +282,11 @@ function CreateFasilitasKesehatan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx
index 137b1842..b21142cf 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx
@@ -29,6 +29,20 @@ function EditDokterTenagaMedis() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.specialist?.trim() !== '' &&
+ formData.jadwal?.trim() !== '' &&
+ formData.jadwalLibur?.trim() !== '' &&
+ formData.jamBukaOperasional !== '' &&
+ formData.jamTutupOperasional !== '' &&
+ formData.jamBukaLibur !== '' &&
+ formData.jamTutupLibur !== ''
+ );
+ };
+
const [formData, setFormData] = useState({
name: '',
specialist: '',
@@ -108,6 +122,46 @@ function EditDokterTenagaMedis() {
// Submit
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama dokter wajib diisi');
+ return;
+ }
+
+ if (!formData.specialist?.trim()) {
+ toast.error('Specialist wajib diisi');
+ return;
+ }
+
+ if (!formData.jadwal?.trim()) {
+ toast.error('Jadwal wajib diisi');
+ return;
+ }
+
+ if (!formData.jadwalLibur?.trim()) {
+ toast.error('Jadwal libur wajib diisi');
+ return;
+ }
+
+ if (!formData.jamBukaOperasional) {
+ toast.error('Jam buka operasional wajib diisi');
+ return;
+ }
+
+ if (!formData.jamTutupOperasional) {
+ toast.error('Jam tutup operasional wajib diisi');
+ return;
+ }
+
+ if (!formData.jamBukaLibur) {
+ toast.error('Jam buka hari libur wajib diisi');
+ return;
+ }
+
+ if (!formData.jamTutupLibur) {
+ toast.error('Jam tutup hari libur wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
state.update.form = { ...state.update.form, ...formData };
@@ -223,8 +277,11 @@ function EditDokterTenagaMedis() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx
index c447da01..4b5f8666 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx
@@ -16,6 +16,20 @@ function CreateDokter() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.create.form.name?.trim() !== '' &&
+ createState.create.create.form.specialist?.trim() !== '' &&
+ createState.create.create.form.jadwal?.trim() !== '' &&
+ createState.create.create.form.jadwalLibur?.trim() !== '' &&
+ createState.create.create.form.jamBukaOperasional !== '' &&
+ createState.create.create.form.jamTutupOperasional !== '' &&
+ createState.create.create.form.jamBukaLibur !== '' &&
+ createState.create.create.form.jamTutupLibur !== ''
+ );
+ };
+
const resetForm = () => {
createState.create.create.form = {
name: "",
@@ -41,7 +55,49 @@ function CreateDokter() {
);
- const handleSubmit = async () => {
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault(); // Prevent default form submission
+
+ if (!createState.create.create.form.name?.trim()) {
+ toast.error('Nama dokter wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.specialist?.trim()) {
+ toast.error('Specialist wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.jadwal?.trim()) {
+ toast.error('Jadwal wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.jadwalLibur?.trim()) {
+ toast.error('Jadwal libur wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.jamBukaOperasional) {
+ toast.error('Jam buka operasional wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.jamTutupOperasional) {
+ toast.error('Jam tutup operasional wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.jamBukaLibur) {
+ toast.error('Jam buka hari libur wajib diisi');
+ return;
+ }
+
+ if (!createState.create.create.form.jamTutupLibur) {
+ toast.error('Jam tutup hari libur wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await createState.create.create.create();
@@ -170,8 +226,11 @@ function CreateDokter() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx
index 3be7c9fa..4f291b8d 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx
@@ -25,6 +25,14 @@ function EditTarifLayanan() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.layanan?.trim() !== '' &&
+ formData.tarif?.trim() !== ''
+ );
+ };
+
const [originalData, setOriginalData] = useState({
tarif: '',
layanan: ''
@@ -74,6 +82,16 @@ function EditTarifLayanan() {
};
const handleSubmit = async () => {
+ if (!formData.layanan?.trim()) {
+ toast.error('Layanan wajib diisi');
+ return;
+ }
+
+ if (!formData.tarif?.trim()) {
+ toast.error('Tarif wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// update global state hanya saat submit
@@ -155,8 +173,11 @@ function EditTarifLayanan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx
index 2fa033fd..94aaee98 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx
@@ -22,6 +22,14 @@ function CreateTarifLayanan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.layanan?.trim() !== '' &&
+ createState.create.form.tarif?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
createState.create.form = {
tarif: '',
@@ -30,6 +38,16 @@ function CreateTarifLayanan() {
};
const handleSubmit = async () => {
+ if (!createState.create.form.layanan?.trim()) {
+ toast.error('Layanan wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.tarif?.trim()) {
+ toast.error('Tarif wajib diisi');
+ return;
+ }
+
setIsSubmitting(true);
try {
await createState.create.create();
@@ -101,8 +119,11 @@ function CreateTarifLayanan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx
index 3e1cf301..e04e0763 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx
@@ -43,6 +43,25 @@ function EditJadwalKegiatan() {
const [formData, setFormData] = useState(emptyForm());
const [originalData, setOriginalData] = useState(emptyForm());
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.content?.trim() !== '' &&
+ formData.informasiJadwalKegiatan.name?.trim() !== '' &&
+ formData.informasiJadwalKegiatan.tanggal?.trim() !== '' &&
+ formData.informasiJadwalKegiatan.waktu?.trim() !== '' &&
+ formData.informasiJadwalKegiatan.lokasi?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsiJadwalKegiatan.deskripsi)
+ );
+ };
+
// Helper untuk update nested state
const updateNested = <
@@ -241,8 +260,11 @@ function EditJadwalKegiatan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx
index 8824ac8e..824ed933 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx
@@ -24,6 +24,25 @@ function CreateJadwalKegiatan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateJadwalKegiatan.create.form.content?.trim() !== '' &&
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name?.trim() !== '' &&
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal?.trim() !== '' &&
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu?.trim() !== '' &&
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi?.trim() !== '' &&
+ !isHtmlEmpty(stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi)
+ );
+ };
+
const resetForm = () => {
stateJadwalKegiatan.create.form = {
content: '',
@@ -198,8 +217,11 @@ function CreateJadwalKegiatan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx
index 8fb38ec1..ef9e30b4 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx
@@ -26,6 +26,17 @@ function EditGrafikHasilKepuasan() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.tanggal !== '' &&
+ formData.jenisKelamin?.trim() !== '' &&
+ formData.alamat?.trim() !== '' &&
+ formData.penyakit?.trim() !== ''
+ );
+ };
+
const [formData, setFormData] = useState({
nama: '',
tanggal: '',
@@ -95,6 +106,31 @@ function EditGrafikHasilKepuasan() {
};
const handleSubmit = async () => {
+ if (!formData.nama?.trim()) {
+ toast.error('Nama wajib diisi');
+ return;
+ }
+
+ if (!formData.tanggal) {
+ toast.error('Tanggal wajib diisi');
+ return;
+ }
+
+ if (!formData.jenisKelamin?.trim()) {
+ toast.error('Jenis kelamin wajib diisi');
+ return;
+ }
+
+ if (!formData.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
+ if (!formData.penyakit?.trim()) {
+ toast.error('Penyakit wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
editState.update.form = { ...editState.update.form, ...formData };
@@ -164,8 +200,11 @@ function EditGrafikHasilKepuasan() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx
index 3a5deb37..caf68c3e 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx
@@ -25,6 +25,17 @@ function CreateGrafikHasilKepuasanMasyarakat() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateGrafikKepuasan.create.form.nama?.trim() !== '' &&
+ stateGrafikKepuasan.create.form.tanggal !== '' &&
+ stateGrafikKepuasan.create.form.jenisKelamin?.trim() !== '' &&
+ stateGrafikKepuasan.create.form.alamat?.trim() !== '' &&
+ stateGrafikKepuasan.create.form.penyakit?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stateGrafikKepuasan.create.form = {
nama: "",
@@ -36,6 +47,31 @@ function CreateGrafikHasilKepuasanMasyarakat() {
};
const handleSubmit = async () => {
+ if (!stateGrafikKepuasan.create.form.nama?.trim()) {
+ toast.error('Nama wajib diisi');
+ return;
+ }
+
+ if (!stateGrafikKepuasan.create.form.tanggal) {
+ toast.error('Tanggal wajib diisi');
+ return;
+ }
+
+ if (!stateGrafikKepuasan.create.form.jenisKelamin?.trim()) {
+ toast.error('Jenis kelamin wajib diisi');
+ return;
+ }
+
+ if (!stateGrafikKepuasan.create.form.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
+ if (!stateGrafikKepuasan.create.form.penyakit?.trim()) {
+ toast.error('Penyakit wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await stateGrafikKepuasan.create.create();
@@ -129,8 +165,11 @@ function CreateGrafikHasilKepuasanMasyarakat() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx
index 5cb84c0b..de7c964e 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx
@@ -26,6 +26,16 @@ function EditKelahiran() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.tanggal !== '' &&
+ formData.jenisKelamin?.trim() !== '' &&
+ formData.alamat?.trim() !== ''
+ );
+ };
+
const [formData, setFormData] = useState({
nama: '',
tanggal: '',
@@ -90,6 +100,26 @@ function EditKelahiran() {
const handleSubmit = async () => {
+ if (!formData.nama?.trim()) {
+ toast.error('Nama wajib diisi');
+ return;
+ }
+
+ if (!formData.tanggal) {
+ toast.error('Tanggal wajib diisi');
+ return;
+ }
+
+ if (!formData.jenisKelamin?.trim()) {
+ toast.error('Jenis kelamin wajib diisi');
+ return;
+ }
+
+ if (!formData.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Update global state hanya saat submit
@@ -173,8 +203,11 @@ function EditKelahiran() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx
index 559110ba..5c2d2d5e 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx
@@ -24,6 +24,16 @@ function CreateKelahiran() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.nama?.trim() !== '' &&
+ createState.create.form.tanggal !== '' &&
+ createState.create.form.jenisKelamin?.trim() !== '' &&
+ createState.create.form.alamat?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
createState.create.form = {
nama: '',
@@ -35,6 +45,26 @@ function CreateKelahiran() {
const handleSubmit = async () => {
+ if (!createState.create.form.nama?.trim()) {
+ toast.error('Nama wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.tanggal) {
+ toast.error('Tanggal wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.jenisKelamin?.trim()) {
+ toast.error('Jenis kelamin wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
await createState.create.create();
@@ -126,8 +156,11 @@ function CreateKelahiran() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx
index 72dac9dc..3e58c773 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx
@@ -28,6 +28,24 @@ function EditKematian() {
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.tanggal !== '' &&
+ formData.jenisKelamin?.trim() !== '' &&
+ formData.alamat?.trim() !== '' &&
+ !isHtmlEmpty(formData.penyebab)
+ );
+ };
+
const [formData, setFormData] = useState({
nama: '',
tanggal: '',
@@ -96,6 +114,31 @@ function EditKematian() {
};
const handleSubmit = async () => {
+ if (!formData.nama?.trim()) {
+ toast.error('Nama wajib diisi');
+ return;
+ }
+
+ if (!formData.tanggal) {
+ toast.error('Tanggal wajib diisi');
+ return;
+ }
+
+ if (!formData.jenisKelamin?.trim()) {
+ toast.error('Jenis kelamin wajib diisi');
+ return;
+ }
+
+ if (!formData.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.penyebab)) {
+ toast.error('Penyebab wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
// Update global state saat submit
@@ -194,8 +237,11 @@ function EditKematian() {
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, #eeeeee)`
+ : `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)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx
index 37cbd016..c742c297 100644
--- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx
@@ -23,6 +23,24 @@ function CreateKematian() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.nama?.trim() !== '' &&
+ createState.create.form.tanggal !== '' &&
+ createState.create.form.jenisKelamin?.trim() !== '' &&
+ createState.create.form.alamat?.trim() !== '' &&
+ !isHtmlEmpty(createState.create.form.penyebab)
+ );
+ };
+
const resetForm = () => {
createState.create.form = {
nama: '',
@@ -35,16 +53,33 @@ function CreateKematian() {
const handleSubmit = async () => {
+ if (!createState.create.form.nama?.trim()) {
+ toast.error('Nama wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.tanggal) {
+ toast.error('Tanggal wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.jenisKelamin?.trim()) {
+ toast.error('Jenis kelamin wajib diisi');
+ return;
+ }
+
+ if (!createState.create.form.alamat?.trim()) {
+ toast.error('Alamat wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(createState.create.form.penyebab)) {
+ toast.error('Penyebab wajib diisi');
+ return;
+ }
+
try {
setIsSubmitting(true);
- if (!createState.create.form.nama) {
- return toast.warn('Nama wajib diisi');
- }
- if (!createState.create.form.tanggal) {
- return toast.warn('Tanggal wajib diisi');
- }
-
-
await createState.create.create();
resetForm();
router.push(
@@ -140,8 +175,11 @@ function CreateKematian() {
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, #eeeeee)`
+ : `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)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx
index 286a6e29..34c07fa7 100644
--- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx
@@ -47,6 +47,22 @@ function EditInfoWabahPenyakit() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsiSingkat) &&
+ !isHtmlEmpty(formData.deskripsiLengkap)
+ );
+ };
+
// Helper untuk update field formData
const updateField = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
@@ -274,8 +290,11 @@ function EditInfoWabahPenyakit() {
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, #eeeeee)`
+ : `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)/kesehatan/info-wabah-penyakit/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx
index 7e2c682c..801dd945 100644
--- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx
@@ -30,6 +30,23 @@ function CreateInfoWabahPenyakit() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ infoWabahPenyakitState.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(infoWabahPenyakitState.create.form.deskripsiSingkat) &&
+ !isHtmlEmpty(infoWabahPenyakitState.create.form.deskripsiLengkap) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
infoWabahPenyakitState.create.form = {
name: "",
@@ -216,8 +233,11 @@ function CreateInfoWabahPenyakit() {
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, #eeeeee)`
+ : `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)/kesehatan/kontak-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx
index e3459054..39d3035e 100644
--- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx
@@ -47,6 +47,22 @@ function EditKontakDarurat() {
});
const [loading, setLoading] = useState(true);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.whatsapp?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data sekali saat mount
useEffect(() => {
const loadKontakDarurat = async () => {
@@ -256,8 +272,11 @@ function EditKontakDarurat() {
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, #eeeeee)`
+ : `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)/kesehatan/kontak-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx
index 9befb923..c1af6df8 100644
--- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx
@@ -35,6 +35,23 @@ function CreateKontakDarurat() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ kontakDaruratState.create.form.name?.trim() !== '' &&
+ kontakDaruratState.create.form.whatsapp?.trim() !== '' &&
+ !isHtmlEmpty(kontakDaruratState.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
kontakDaruratState.create.form = {
name: '',
@@ -240,8 +257,11 @@ function CreateKontakDarurat() {
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, #eeeeee)`
+ : `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)/kesehatan/penanganan-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx
index 1cb79f35..a637af41 100644
--- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx
@@ -48,6 +48,21 @@ function EditPenangananDarurat() {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(true);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data satu kali saat component mount
useEffect(() => {
const loadData = async () => {
@@ -263,8 +278,11 @@ function EditPenangananDarurat() {
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, #eeeeee)`
+ : `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)/kesehatan/penanganan-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx
index 74f4f434..174a396c 100644
--- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx
@@ -35,6 +35,22 @@ function CreatePenangananDarurat() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [file, setFile] = useState(null);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ penangananDaruratState.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(penangananDaruratState.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
penangananDaruratState.create.form = {
name: '',
@@ -234,8 +250,11 @@ function CreatePenangananDarurat() {
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, #eeeeee)`
+ : `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)/kesehatan/posyandu/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx
index 61422ed9..69b7e102 100644
--- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx
@@ -33,6 +33,25 @@ function EditPosyandu() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.nomor?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ !isHtmlEmpty(formData.jadwalPelayanan) &&
+ (file !== null || originalData.imageId !== '') // Either a new file is selected or an existing image exists
+ );
+ };
+
const [formData, setFormData] = useState({
name: '',
nomor: '',
@@ -84,6 +103,31 @@ function EditPosyandu() {
}, [params?.id]);
const handleSubmit = async () => {
+ if (!formData.name?.trim()) {
+ toast.error('Nama posyandu wajib diisi');
+ return;
+ }
+
+ if (!formData.nomor?.trim()) {
+ toast.error('Nomor telepon wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(formData.jadwalPelayanan)) {
+ toast.error('Jadwal pelayanan wajib diisi');
+ return;
+ }
+
+ if (!file && !originalData.imageId) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
const updatedForm = { ...statePosyandu.edit.form, ...formData };
@@ -278,8 +322,11 @@ function EditPosyandu() {
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, #eeeeee)`
+ : `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)/kesehatan/posyandu/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx
index 63c6ec28..d58cabc9 100644
--- a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx
@@ -31,6 +31,23 @@ function CreatePosyandu() {
const [previewImage, setPreviewImage] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ statePosyandu.create.form.name?.trim() !== '' &&
+ statePosyandu.create.form.nomor?.trim() !== '' &&
+ !isHtmlEmpty(statePosyandu.create.form.deskripsi) &&
+ !isHtmlEmpty(statePosyandu.create.form.jadwalPelayanan) &&
+ file !== null
+ );
+ };
const resetForm = () => {
statePosyandu.create.form = {
@@ -46,6 +63,31 @@ function CreatePosyandu() {
const handleSubmit = async () => {
+ if (!statePosyandu.create.form.name?.trim()) {
+ toast.error('Nama posyandu wajib diisi');
+ return;
+ }
+
+ if (!statePosyandu.create.form.nomor?.trim()) {
+ toast.error('Nomor telepon wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(statePosyandu.create.form.deskripsi)) {
+ toast.error('Deskripsi wajib diisi');
+ return;
+ }
+
+ if (isHtmlEmpty(statePosyandu.create.form.jadwalPelayanan)) {
+ toast.error('Jadwal pelayanan wajib diisi');
+ return;
+ }
+
+ if (!file) {
+ toast.error('Gambar wajib dipilih');
+ return;
+ }
+
try {
setIsSubmitting(true);
if (!file) {
@@ -221,8 +263,11 @@ function CreatePosyandu() {
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, #eeeeee)`
+ : `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)/kesehatan/program-kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx
index f3edf417..d3f48814 100644
--- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx
@@ -46,6 +46,22 @@ function EditProgramKesehatan() {
imageUrl: ''
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsiSingkat) &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data awal
useEffect(() => {
const loadData = async () => {
@@ -266,8 +282,11 @@ function EditProgramKesehatan() {
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, #eeeeee)`
+ : `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)/kesehatan/program-kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx
index 1e4542c2..ba3f9ee4 100644
--- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx
@@ -30,6 +30,23 @@ function CreateProgramKesehatan() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ programKesehatanState.create.form.name?.trim() !== '' &&
+ !isHtmlEmpty(programKesehatanState.create.form.deskripsiSingkat) &&
+ !isHtmlEmpty(programKesehatanState.create.form.deskripsi) &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
programKesehatanState.create.form = {
name: "",
@@ -223,8 +240,11 @@ function CreateProgramKesehatan() {
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, #eeeeee)`
+ : `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)/kesehatan/puskesmas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx
index 93fcc4d0..7869b690 100644
--- a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx
@@ -72,6 +72,17 @@ function EditPuskesmas() {
imageUrl: ''
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.alamat?.trim() !== '' &&
+ formData.jam.workDays?.trim() !== '' &&
+ formData.jam.weekDays?.trim() !== '' &&
+ formData.jam.holiday?.trim() !== ''
+ );
+ };
+
useEffect(() => {
const loadPuskesmas = async () => {
const id = params?.id as string;
@@ -383,8 +394,11 @@ function EditPuskesmas() {
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, #eeeeee)`
+ : `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)/kesehatan/puskesmas/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx
index f75eb7ea..a2c14d97 100644
--- a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx
+++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx
@@ -29,6 +29,15 @@ function CreatePuskesmas() {
const [previewImage, setPreviewImage] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ statePuskesmas.create.form.name?.trim() !== '' &&
+ statePuskesmas.create.form.alamat?.trim() !== '' &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
statePuskesmas.create.form = {
name: '',
@@ -257,8 +266,11 @@ function CreatePuskesmas() {
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, #eeeeee)`
+ : `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/[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..2c84dda3 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);
@@ -68,33 +76,62 @@ function EditAPBDes() {
tipe: 'pendapatan',
});
- // Type for the API response
- interface APBDesResponse {
- id: string;
- image?: {
- link: string;
- id: string;
- };
- file?: {
- link: string;
- id: string;
- };
- // Add other properties as needed
- }
+ // Simpan data original untuk reset form
+ const [originalData, setOriginalData] = useState({
+ tahun: 0,
+ imageId: '',
+ fileId: '',
+ imageUrl: '',
+ fileUrl: '',
+ });
// Load data saat pertama kali
useEffect(() => {
const id = params?.id as string;
- if (id) {
- apbdesState.edit.load(id).then((response) => {
- const data = response as unknown as APBDesResponse;
- if (data) {
- // ✅ Ambil link langsung dari response
- setPreviewImage(data.image?.link || null);
- setPreviewDoc(data.file?.link || null);
- }
- });
- }
+ if (!id) return;
+
+ const loadData = async () => {
+ try {
+ const data = await apbdesState.edit.load(id);
+
+ if (!data) return;
+
+ // Set preview dari data lama
+ setPreviewImage(data.image?.link || null);
+ setPreviewDoc(data.file?.link || null);
+
+ // Simpan data original untuk reset
+ setOriginalData({
+ tahun: data.tahun || new Date().getFullYear(),
+ imageId: data.imageId || '',
+ fileId: data.fileId || '',
+ imageUrl: data.image?.link || '',
+ fileUrl: data.file?.link || '',
+ });
+
+ // Set form dengan data lama (termasuk imageId dan fileId)
+ apbdesState.edit.form = {
+ tahun: data.tahun || new Date().getFullYear(),
+ imageId: data.imageId || '',
+ fileId: data.fileId || '',
+ items: (data.items || []).map((item: any) => ({
+ kode: item.kode,
+ uraian: item.uraian,
+ anggaran: item.anggaran,
+ realisasi: item.realisasi,
+ selisih: item.selisih,
+ persentase: item.persentase,
+ level: item.level,
+ tipe: item.tipe || 'pendapatan',
+ })),
+ };
+ } catch (error) {
+ console.error('Error loading APBDes:', error);
+ toast.error('Gagal memuat data APBDes');
+ }
+ };
+
+ loadData();
}, [params?.id]);
const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => {
@@ -154,23 +191,38 @@ function EditAPBDes() {
try {
setIsSubmitting(true);
- // Upload file baru jika ada
+ // Upload file baru jika ada perubahan
if (imageFile) {
+ // Hapus file lama dari form jika ada file baru
const res = await ApiFetch.api.fileStorage.create.post({
file: imageFile,
name: imageFile.name,
});
const imageId = res.data?.data?.id;
- if (imageId) apbdesState.edit.form.imageId = imageId;
+ if (imageId) {
+ apbdesState.edit.form.imageId = imageId;
+ }
}
if (docFile) {
+ // Hapus file lama dari form jika ada file baru
const res = await ApiFetch.api.fileStorage.create.post({
file: docFile,
name: docFile.name,
});
const fileId = res.data?.data?.id;
- if (fileId) apbdesState.edit.form.fileId = fileId;
+ if (fileId) {
+ apbdesState.edit.form.fileId = fileId;
+ }
+ }
+
+ // Jika tidak ada file baru, gunakan ID lama (sudah ada di form)
+ // Pastikan imageId dan fileId tetap ada
+ if (!apbdesState.edit.form.imageId) {
+ return toast.warn('Gambar wajib diunggah');
+ }
+ if (!apbdesState.edit.form.fileId) {
+ return toast.warn('Dokumen wajib diunggah');
}
const success = await apbdesState.edit.update();
@@ -186,21 +238,33 @@ function EditAPBDes() {
};
const handleReset = () => {
- const id = params?.id as string;
- if (id) {
- apbdesState.edit.load(id);
- setImageFile(null);
- setDocFile(null);
- setNewItem({
- kode: '',
- uraian: '',
- anggaran: 0,
- realisasi: 0,
- level: 1,
- tipe: 'pendapatan',
- });
- toast.info('Form dikembalikan ke data awal');
- }
+ // Reset ke data original (tahun, imageId, fileId)
+ apbdesState.edit.form = {
+ tahun: originalData.tahun,
+ imageId: originalData.imageId,
+ fileId: originalData.fileId,
+ items: [...apbdesState.edit.form.items], // keep existing items
+ };
+
+ // Reset preview ke data original
+ setPreviewImage(originalData.imageUrl || null);
+ setPreviewDoc(originalData.fileUrl || null);
+
+ // Reset file uploads
+ setImageFile(null);
+ setDocFile(null);
+
+ // Reset new item form
+ setNewItem({
+ kode: '',
+ uraian: '',
+ anggaran: 0,
+ realisasi: 0,
+ level: 1,
+ tipe: 'pendapatan',
+ });
+
+ toast.info('Form dikembalikan ke data awal');
};
return (
@@ -492,9 +556,11 @@ function EditAPBDes() {
diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
index 80cdad96..1ac3945a 100644
--- a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
@@ -47,6 +47,15 @@ function CreateAPBDes() {
const [docFile, setDocFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ imageFile !== null &&
+ docFile !== null &&
+ stateAPBDes.create.form.items.length > 0
+ );
+ };
+
// Form sementara untuk input item baru
const [newItem, setNewItem] = useState({
kode: '',
@@ -468,9 +477,11 @@ function CreateAPBDes() {
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx
index 548e649f..d3068ef7 100644
--- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx
@@ -25,6 +25,11 @@ export default function EditKategoriDesaAntiKorupsi() {
const [originalData, setOriginalData] = useState({ name: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.name?.trim() !== '';
+ };
+
// 📥 load data saat pertama kali dibuka
useEffect(() => {
if (!id) return;
@@ -126,9 +131,11 @@ export default function EditKategoriDesaAntiKorupsi() {
onClick={handleSubmit}
radius="md"
size="md"
- disabled={isSubmitting}
+ 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/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx
index 40ead798..5425b45f 100644
--- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx
@@ -14,6 +14,11 @@ export default function CreateKategoriDesaAntiKorupsi() {
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return stateKategori.create.form.name?.trim() !== '';
+ };
+
useEffect(() => {
stateKategori.findMany.load();
}, []);
@@ -87,8 +92,11 @@ export default function CreateKategoriDesaAntiKorupsi() {
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/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx
index 423a1f99..3ac9761b 100644
--- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx
@@ -36,6 +36,15 @@ export default function EditDesaAntiKorupsi() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.kategoriId?.trim() !== '' &&
+ (formData.fileId?.trim() !== '' || file !== null)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
@@ -287,8 +296,11 @@ export default function EditDesaAntiKorupsi() {
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/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx
index 5d58a95c..6068ed8a 100644
--- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx
@@ -31,6 +31,15 @@ export default function CreateDesaAntiKorupsi() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ file !== null &&
+ stateKorupsi.create.form.name?.trim() !== '' &&
+ stateKorupsi.create.form.kategoriId?.trim() !== ''
+ );
+ };
+
useEffect(() => {
stateKorupsi.findMany.load();
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
@@ -238,8 +247,11 @@ export default function CreateDesaAntiKorupsi() {
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/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx
index b0128513..fe4515d6 100644
--- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx
@@ -21,6 +21,11 @@ function EditKategoriPrestasi() {
const [resetData, setResetData] = useState({ name: '' });
const [loading, setLoading] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.name?.trim() !== '';
+ };
+
// Load data kategori prestasi saat component mount
useEffect(() => {
if (!id) return;
@@ -123,8 +128,11 @@ function EditKategoriPrestasi() {
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/prestasi-desa/kategori-prestasi-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx
index 8e41927b..32a5071a 100644
--- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx
@@ -15,6 +15,11 @@ function CreateKategoriPrestasi() {
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return stateKategori.create.form.name?.trim() !== '';
+ };
+
useEffect(() => {
stateKategori.findMany.load();
}, []);
@@ -84,8 +89,11 @@ function CreateKategoriPrestasi() {
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/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx
index 402ede7e..70bb513a 100644
--- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/edit/page.tsx
@@ -31,6 +31,15 @@ export default function EditPrestasiDesa() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.kategoriId?.trim() !== '' &&
+ (formData.imageId?.trim() !== '' || file !== null)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
@@ -253,8 +262,11 @@ export default function EditPrestasiDesa() {
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/prestasi-desa/list-prestasi-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx
index 450a8f59..c27240e9 100644
--- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx
@@ -21,6 +21,14 @@ function CreatePrestasiDesa() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ file !== null &&
+ snapPrestasi.create.form.name?.trim() !== '' &&
+ snapPrestasi.create.form.kategoriId?.trim() !== ''
+ );
+ };
useEffect(() => {
prestasi.findMany.load();
@@ -226,8 +234,11 @@ function CreatePrestasiDesa() {
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/profil/media-sosial/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx
index e1d59050..46582b2d 100644
--- a/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx
@@ -70,6 +70,15 @@ function EditMediaSosial() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ const isNameValid = formData.name?.trim() !== '';
+ const isIconUrlValid = formData.iconUrl?.trim() !== '';
+ const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
+
+ return isNameValid && isIconUrlValid && isCustomIconValid;
+ };
+
const [originalData, setOriginalData] = useState({
name: '',
icon: '',
@@ -340,8 +349,11 @@ function EditMediaSosial() {
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/profil/media-sosial/create/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/create/page.tsx
index 17e1c952..1de44ae4 100644
--- a/src/app/admin/(dashboard)/landing-page/profil/media-sosial/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/profil/media-sosial/create/page.tsx
@@ -38,20 +38,6 @@ type SosmedKey =
| 'telephone'
| 'custom';
-// ⭐ mapping icon sosmed bawaan
-const sosmedMap: Record = {
- facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
- instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
- tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
- youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
- whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
- gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
- telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
- x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
- telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
- custom: { label: 'Custom Icon', src: null },
-};
-
export default function CreateMediaSosial() {
const router = useRouter();
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
@@ -61,6 +47,15 @@ export default function CreateMediaSosial() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ const isNameValid = stateMediaSosial.create.form.name?.trim() !== '';
+ const isIconUrlValid = stateMediaSosial.create.form.iconUrl?.trim() !== '';
+ const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
+
+ return isNameValid && isIconUrlValid && isCustomIconValid;
+ };
+
useEffect(() => {
stateMediaSosial.findMany.load();
}, []);
@@ -85,7 +80,7 @@ export default function CreateMediaSosial() {
// ──────────────── ⭐ CASE 1: PAKAI ICON DEFAULT ────────────────
if (selectedSosmed !== 'custom') {
stateMediaSosial.create.form.imageId = null;
- stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
+ stateMediaSosial.create.form.icon = selectedSosmed; // Store the key, not the path
await stateMediaSosial.create.create();
resetForm();
@@ -261,8 +256,11 @@ export default function CreateMediaSosial() {
(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.position?.trim() !== '' &&
+ (formData.imageId?.trim() !== '' || file !== null)
+ );
+ };
+
// Load data on mount
useEffect(() => {
const loadData = async () => {
@@ -327,11 +336,13 @@ function EditPejabatDesa() {
{
+ return (
+ formData.name?.trim() !== '' &&
+ formData.description?.trim() !== '' &&
+ formData.link?.trim() !== '' &&
+ (formData.imageId?.trim() !== '' || file !== null)
+ );
+ };
+
const [originalData, setOriginalData] = useState({
name: "",
description: "",
@@ -271,8 +281,11 @@ function EditProgramInovasi() {
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/profil/program-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx
index c2e26cf6..e0a7cf84 100644
--- a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx
@@ -8,6 +8,7 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
+import DOMPurify from 'dompurify';
function DetailProgramInovasi() {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
@@ -85,7 +86,7 @@ function DetailProgramInovasi() {
Deskripsi
-
+
diff --git a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/create/page.tsx
index eac0393f..a3612885 100644
--- a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/create/page.tsx
@@ -31,6 +31,16 @@ function CreateProgramInovasi() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateProgramInovasi.create.form.name?.trim() !== '' &&
+ stateProgramInovasi.create.form.description?.trim() !== '' &&
+ stateProgramInovasi.create.form.link?.trim() !== '' &&
+ file !== null
+ );
+ };
+
useEffect(() => {
stateProgramInovasi.findMany.load();
}, []);
@@ -221,8 +231,11 @@ function CreateProgramInovasi() {
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/profil/program-inovasi/page.tsx b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx
index b2c30db0..0c68b120 100644
--- a/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx
+++ b/src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx
@@ -6,6 +6,7 @@ import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
+import DOMPurify from 'dompurify';
import HeaderSearch from '../../../_com/header';
import profileLandingPageState from '../../../_state/landing-page/profile';
@@ -90,7 +91,7 @@ function ListProgramInovasi({ search }: { search: string }) {
{item.name}
-
+
@@ -144,7 +145,7 @@ function ListProgramInovasi({ search }: { search: string }) {
{/* Description */}
Deskripsi
-
+
{/* Link */}
diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx
index c04f6acb..7aa9768c 100644
--- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx
@@ -67,6 +67,23 @@ export default function EditDataLingkunganDesa() {
icon: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.jumlah?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.icon?.trim() !== ''
+ );
+ };
+
// Load data saat komponen mount
useEffect(() => {
const loadData = async () => {
@@ -211,8 +228,11 @@ export default function EditDataLingkunganDesa() {
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, #eeeeee)`
+ : `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)/lingkungan/data-lingkungan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx
index 41e20ec8..b03802cb 100644
--- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx
@@ -25,6 +25,23 @@ function CreateDataLingkunganDesa() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.name?.trim() !== '' &&
+ stateCreate.create.form.icon?.trim() !== '' &&
+ stateCreate.create.form.jumlah?.trim() !== '' &&
+ !isHtmlEmpty(stateCreate.create.form.deskripsi)
+ );
+ };
+
const resetForm = () => {
stateCreate.create.form = {
name: '',
@@ -129,8 +146,11 @@ function CreateDataLingkunganDesa() {
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, #eeeeee)`
+ : `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)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx
index 1dbf993d..5808250d 100644
--- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx
@@ -38,6 +38,21 @@ export default function EditContohKegiatanDesaDarmasaba() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.judul) &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// load data awal
useShallowEffect(() => {
if (!contohEdukasiState.findById.data) {
@@ -156,8 +171,11 @@ export default function EditContohKegiatanDesaDarmasaba() {
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, #eeeeee)`
+ : `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)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx
index 5614e6bf..253b11ea 100644
--- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx
@@ -27,6 +27,21 @@ export default function EditMateriEdukasiYangDiberikan() {
content: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.judul) &&
+ !isHtmlEmpty(formData.content)
+ );
+ };
+
// Initialize data kalau belum ada
useShallowEffect(() => {
if (!materiEdukasiState.findById.data) {
@@ -139,8 +154,11 @@ export default function EditMateriEdukasiYangDiberikan() {
onClick={submit}
radius="md"
size="md"
+ disabled={!isFormValid() || isSubmitting}
style={{
- background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
+ background: !isFormValid() || isSubmitting
+ ? `linear-gradient(135deg, #cccccc, #eeeeee)`
+ : `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)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx
index a1e5952a..0e673e9a 100644
--- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx
@@ -28,6 +28,21 @@ export default function EditTujuanEdukasiLingkungan() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.judul) &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Initialize global state
useShallowEffect(() => {
if (!tujuanEdukasiState.findById.data) {
@@ -147,8 +162,11 @@ export default function EditTujuanEdukasiLingkungan() {
onClick={submit}
radius="md"
size="md"
+ disabled={!isFormValid() || isSubmitting}
style={{
- background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
+ background: !isFormValid() || isSubmitting
+ ? `linear-gradient(135deg, #cccccc, #eeeeee)`
+ : `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)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx
index aa1b6a11..f7f67a9d 100644
--- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx
@@ -21,6 +21,11 @@ function EditKategoriKegiatan() {
const [originalData, setOriginalData] = useState({ nama: '' });
const [loading, setLoading] = useState(true);
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.nama?.trim() !== '';
+ };
+
// Load data once
useEffect(() => {
if (!id) return;
@@ -126,8 +131,11 @@ function EditKategoriKegiatan() {
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, #eeeeee)`
+ : `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)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx
index 9b068f04..ab830bb0 100644
--- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx
@@ -14,6 +14,11 @@ function CreateKategoriKegiatan() {
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan)
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return stateKategori.create.form.nama?.trim() !== '';
+ };
+
useEffect(() => {
stateKategori.findMany.load();
}, []);
@@ -84,8 +89,11 @@ function CreateKategoriKegiatan() {
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, #eeeeee)`
+ : `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)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx
index 870d7f3b..b7fd995c 100644
--- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx
@@ -67,6 +67,27 @@ export default function EditKegiatanDesa() {
const [file, setFile] = useState(null);
const [previewImage, setPreviewImage] = useState(null);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsiSingkat) &&
+ !isHtmlEmpty(formData.deskripsiLengkap) &&
+ formData.tanggal?.trim() !== '' &&
+ formData.lokasi?.trim() !== '' &&
+ formData.partisipan !== null &&
+ formData.partisipan >= 0 &&
+ formData.kategoriKegiatanId?.trim() !== ''
+ );
+ };
+
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
return new Date(dateString).toISOString().split('T')[0];
@@ -312,8 +333,11 @@ export default function EditKegiatanDesa() {
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, #eeeeee)`
+ : `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)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx
index 10ae2d89..a65cbb1c 100644
--- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx
@@ -38,6 +38,28 @@ function CreateKegiatanDesa() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateKegiatanDesa.create.form.judul?.trim() !== '' &&
+ !isHtmlEmpty(stateKegiatanDesa.create.form.deskripsiSingkat) &&
+ stateKegiatanDesa.create.form.partisipan !== null &&
+ stateKegiatanDesa.create.form.partisipan >= 0 &&
+ stateKegiatanDesa.create.form.tanggal !== null &&
+ stateKegiatanDesa.create.form.lokasi?.trim() !== '' &&
+ !isHtmlEmpty(stateKegiatanDesa.create.form.deskripsiLengkap) &&
+ stateKegiatanDesa.create.form.kategoriKegiatanId?.trim() !== '' &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateKegiatanDesa.create.form = {
judul: '',
@@ -273,8 +295,11 @@ function CreateKegiatanDesa() {
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, #eeeeee)`
+ : `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)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx
index 9a1821c5..201691b0 100644
--- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx
@@ -27,6 +27,21 @@ function EditBentukKonservasiBerdasarkanAdat() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.judul) &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Initialize data dari global state
useShallowEffect(() => {
if (!bentukKonservasiState.findById.data) {
@@ -137,8 +152,11 @@ function EditBentukKonservasiBerdasarkanAdat() {
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, #eeeeee)`
+ : `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)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx
index df288ba9..5966939a 100644
--- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx
@@ -31,6 +31,21 @@ function EditFilosofiTriHitaKarana() {
content: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.judul) &&
+ !isHtmlEmpty(formData.content)
+ );
+ };
+
// Load data dari global state kalau belum ada
useShallowEffect(() => {
if (!filosofiTriHitaState.findById.data) {
@@ -142,8 +157,11 @@ function EditFilosofiTriHitaKarana() {
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, #eeeeee)`
+ : `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)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx
index 25545db1..01e5bd62 100644
--- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx
@@ -24,6 +24,21 @@ function EditNilaiKonservasiAdat() {
const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isHtmlEmpty(formData.judul) &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// load data awal
useShallowEffect(() => {
if (!nilaiKonservasiState.findById.data) {
@@ -136,8 +151,11 @@ function EditNilaiKonservasiAdat() {
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, #eeeeee)`
+ : `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)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx
index 1db48752..05bd12eb 100644
--- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx
@@ -35,6 +35,16 @@ function EditKeteranganBankSampahTerdekat() {
lng: 0,
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.alamat?.trim() !== '' &&
+ formData.namaTempatMaps?.trim() !== '' &&
+ markerPosition !== null
+ );
+ };
+
// Load data ketika component mount
useEffect(() => {
const loadKeterangan = async () => {
@@ -197,8 +207,11 @@ function EditKeteranganBankSampahTerdekat() {
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, #eeeeee)`
+ : `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)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx
index 973665ac..ab97ef36 100644
--- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx
@@ -19,6 +19,16 @@ function CreateKeteranganBankSampahTerdekat() {
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ keteranganState.create.form.name?.trim() !== '' &&
+ keteranganState.create.form.alamat?.trim() !== '' &&
+ keteranganState.create.form.namaTempatMaps?.trim() !== '' &&
+ markerPosition !== null
+ );
+ };
+
const resetForm = () => {
keteranganState.create.form = {
name: "",
@@ -135,8 +145,11 @@ function CreateKeteranganBankSampahTerdekat() {
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, #eeeeee)`
+ : `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)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx
index 5cd5ac57..77be7a07 100644
--- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx
@@ -34,6 +34,14 @@ function EditProgramKreatifDesa() {
icon: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.icon?.trim() !== ''
+ );
+ };
+
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
@@ -143,8 +151,11 @@ function EditProgramKreatifDesa() {
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, #eeeeee)`
+ : `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)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx
index acd88fac..1ed57027 100644
--- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx
@@ -13,6 +13,14 @@ function CreatePengelolaanSampahBankSampah() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.name?.trim() !== '' &&
+ stateCreate.create.form.icon?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stateCreate.create.form = {
name: "",
@@ -91,8 +99,11 @@ function CreatePengelolaanSampahBankSampah() {
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, #eeeeee)`
+ : `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)/lingkungan/program-penghijauan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx
index b8c2b6bb..ba907576 100644
--- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx
@@ -64,6 +64,23 @@ function EditProgramPenghijauan() {
icon: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.icon?.trim() !== ''
+ );
+ };
+
// Load data program penghijauan
useEffect(() => {
const loadProgram = async () => {
@@ -216,8 +233,11 @@ function EditProgramPenghijauan() {
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, #eeeeee)`
+ : `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)/lingkungan/program-penghijauan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx
index 7ea44521..f37f3d73 100644
--- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx
+++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx
@@ -25,6 +25,23 @@ function CreateProgramPenghijauan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.name?.trim() !== '' &&
+ stateCreate.create.form.icon?.trim() !== '' &&
+ stateCreate.create.form.judul?.trim() !== '' &&
+ !isHtmlEmpty(stateCreate.create.form.deskripsi)
+ );
+ };
+
const resetForm = () => {
stateCreate.create.form = {
name: '',
@@ -128,8 +145,11 @@ function CreateProgramPenghijauan() {
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, #eeeeee)`
+ : `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)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx
index 91c42c6a..19e0af87 100644
--- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx
@@ -24,6 +24,21 @@ function EditProgramKreatifDesa() {
deskripsi: '',
})
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
@@ -160,8 +175,11 @@ function EditProgramKreatifDesa() {
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, #eeeeee)`
+ : `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)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx
index c05524a7..5fab03b4 100644
--- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx
@@ -16,6 +16,21 @@ function CreateKeunggulanProgram() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.judul?.trim() !== '' &&
+ !isHtmlEmpty(stateCreate.create.form.deskripsi)
+ );
+ };
+
const resetForm = () => {
stateCreate.create.form = {
judul: "",
@@ -97,8 +112,11 @@ function CreateKeunggulanProgram() {
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, #eeeeee)`
+ : `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)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx
index ebea0187..66f32ada 100644
--- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx
@@ -42,6 +42,21 @@ function EditFasilitasYangDisediakan() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data pertama kali
useShallowEffect(() => {
if (!editState.findById.data) {
@@ -76,11 +91,6 @@ function EditFasilitasYangDisediakan() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -180,8 +190,11 @@ function EditFasilitasYangDisediakan() {
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, #eeeeee)`
+ : `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)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx
index 51b9185e..99325908 100644
--- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx
@@ -39,6 +39,21 @@ function EditLokasiDanJadwal() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data sekali
useShallowEffect(() => {
if (!editState.findById.data) {
@@ -73,11 +88,6 @@ function EditLokasiDanJadwal() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -178,8 +188,11 @@ function EditLokasiDanJadwal() {
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, #eeeeee)`
+ : `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)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx
index 9697085a..af767a98 100644
--- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx
@@ -39,6 +39,21 @@ function EditTujuanProgram() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// load data sekali
useShallowEffect(() => {
if (!editState.findById.data) editState.findById.initialize();
@@ -71,11 +86,6 @@ function EditTujuanProgram() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -170,8 +180,11 @@ function EditTujuanProgram() {
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, #eeeeee)`
+ : `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)/pendidikan/data-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx
index 15e2fd1f..d0ed0615 100644
--- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx
@@ -28,6 +28,14 @@ export default function EditDataPendidikan() {
jumlah: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.name?.trim() !== '' &&
+ formData.jumlah?.trim() !== ''
+ );
+ };
+
// Load data saat mount
useEffect(() => {
if (id) {
@@ -127,8 +135,11 @@ export default function EditDataPendidikan() {
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, #eeeeee)`
+ : `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)/pendidikan/data-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx
index 982c2d0b..2b532948 100644
--- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx
@@ -15,6 +15,14 @@ export default function CreateDataPendidikan() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateDPM.create.form.name?.trim() !== '' &&
+ stateDPM.create.form.jumlah?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stateDPM.create.form = { name: '', jumlah: '' };
};
@@ -90,8 +98,11 @@ export default function CreateDataPendidikan() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx
index 5e90fcdb..8ff89150 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx
@@ -31,6 +31,11 @@ function EditJenjangPendidikan() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.nama?.trim() !== '';
+ };
+
// Load data sekali saat component mount
useEffect(() => {
if (!id) return;
@@ -136,8 +141,11 @@ function EditJenjangPendidikan() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx
index 27f52ec5..75f62892 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx
@@ -23,6 +23,11 @@ function CreateJenjangPendidikan() {
const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return stateJenjang.create.form.nama?.trim() !== '';
+ };
+
useEffect(() => {
stateJenjang.findMany.load();
}, []);
@@ -101,8 +106,11 @@ function CreateJenjangPendidikan() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx
index bd1d769e..5a4b3120 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx
@@ -37,6 +37,14 @@ export default function EditLembaga() {
jenjangId: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ form.nama?.trim() !== '' &&
+ form.jenjangId?.trim() !== ''
+ );
+ };
+
// Load jenjang pendidikan dan data lembaga
useEffect(() => {
infoSekolahPaud.jenjangPendidikan.findMany.load();
@@ -161,8 +169,11 @@ export default function EditLembaga() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/lembaga/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx
index 7061d96b..bc085845 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx
@@ -25,6 +25,14 @@ function CreateLembaga() {
const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateLembaga.create.form.nama?.trim() !== '' &&
+ stateLembaga.create.form.jenjangId?.trim() !== ''
+ );
+ };
+
useEffect(() => {
stateLembaga.findMany.load();
infoSekolahPaud.jenjangPendidikan.findMany.load();
@@ -116,8 +124,11 @@ function CreateLembaga() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx
index 0b0922ab..57c8fdb2 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx
@@ -40,6 +40,14 @@ function EditPengajar() {
lembagaId: ''
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.lembagaId?.trim() !== ''
+ );
+ };
+
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
@@ -157,8 +165,11 @@ function EditPengajar() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/pengajar/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx
index 5edea688..9136a6c5 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx
@@ -25,6 +25,14 @@ function CreatePengajar() {
const stateCreate = useProxy(infoSekolahPaud.pengajar);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.nama?.trim() !== '' &&
+ stateCreate.create.form.lembagaId?.trim() !== ''
+ );
+ };
+
useEffect(() => {
stateCreate.findMany.load();
infoSekolahPaud.lembagaPendidikan.findMany.load();
@@ -116,8 +124,11 @@ function CreatePengajar() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx
index e2b8d002..77c3955e 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx
@@ -42,6 +42,14 @@ function EditSiswa() {
lembagaId: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.lembagaId?.trim() !== ''
+ );
+ };
+
// Load data siswa
useEffect(() => {
const loadSiswa = async () => {
@@ -166,8 +174,11 @@ function EditSiswa() {
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, #eeeeee)`
+ : `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)/pendidikan/info-sekolah/siswa/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx
index 97c93a45..cbb0cd65 100644
--- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx
@@ -25,6 +25,14 @@ function CreateSiswa() {
const stateCreate = useProxy(infoSekolahPaud.siswa);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateCreate.create.form.nama?.trim() !== '' &&
+ stateCreate.create.form.lembagaId?.trim() !== ''
+ );
+ };
+
useEffect(() => {
stateCreate.findMany.load();
infoSekolahPaud.lembagaPendidikan.findMany.load();
@@ -115,8 +123,11 @@ function CreateSiswa() {
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, #eeeeee)`
+ : `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)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx
index d066c347..8a27a3a0 100644
--- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx
@@ -37,6 +37,21 @@ function EditJenisProgramYangDiselenggarakan() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({ judul: '', content: '' });
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.content)
+ );
+ };
+
// Load data pertama kali
useShallowEffect(() => {
if (!editState.findById.data) {
@@ -71,11 +86,6 @@ function EditJenisProgramYangDiselenggarakan() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -168,8 +178,11 @@ function EditJenisProgramYangDiselenggarakan() {
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, #eeeeee)`
+ : `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)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx
index c0670869..70ee9412 100644
--- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx
@@ -45,6 +45,21 @@ function EditTempatKegiatan() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// load data pertama kali
useShallowEffect(() => {
if (!editState.findById.data) {
@@ -79,11 +94,6 @@ function EditTempatKegiatan() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -177,8 +187,11 @@ function EditTempatKegiatan() {
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, #eeeeee)`
+ : `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)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx
index 881e88d4..0bf213c2 100644
--- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx
@@ -38,6 +38,21 @@ function EditTujuanProgram() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data pertama kali
useShallowEffect(() => {
if (!editState.findById.data) {
@@ -72,11 +87,6 @@ function EditTujuanProgram() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
if (!editState.findById.data) return;
setIsSubmitting(true);
@@ -163,8 +173,11 @@ function EditTujuanProgram() {
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, #eeeeee)`
+ : `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)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx
index cc694eb8..564072dc 100644
--- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx
@@ -38,6 +38,22 @@ function EditPerpustakaanDigital() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi) &&
+ formData.kategoriId?.trim() !== ''
+ );
+ };
+
// Load kategori & data awal
useEffect(() => {
perpustakaanDigitalState.kategoriBuku.findManyAll.load();
@@ -254,8 +270,11 @@ function EditPerpustakaanDigital() {
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, #eeeeee)`
+ : `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)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx
index 668aa861..552d8c72 100644
--- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx
@@ -18,6 +18,23 @@ function CreateDataPerpustakaan() {
const [file, setFile] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ createState.create.form.judul?.trim() !== '' &&
+ !isHtmlEmpty(createState.create.form.deskripsi) &&
+ createState.create.form.kategoriId?.trim() !== '' &&
+ file !== null
+ );
+ };
+
useEffect(() => {
perpustakaanDigitalState.kategoriBuku.findManyAll.load();
}, []);
@@ -196,8 +213,11 @@ function CreateDataPerpustakaan() {
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, #eeeeee)`
+ : `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)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx
index fcaeae24..f3efe0d9 100644
--- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx
@@ -23,6 +23,11 @@ function EditKategoriBuku() {
const [formData, setFormData] = useState({ name: '' });
const [loading, setLoading] = useState(true);
+ // Check if form is valid
+ const isFormValid = () => {
+ return formData.name?.trim() !== '';
+ };
+
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -120,8 +125,11 @@ function EditKategoriBuku() {
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, #eeeeee)`
+ : `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)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx
index fd38dded..a42a1cb8 100644
--- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx
@@ -13,6 +13,11 @@ function CreateKategoriBuku() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return createState.create.form.name?.trim() !== '';
+ };
+
const resetForm = () => {
createState.create.form = {
name: "",
@@ -81,8 +86,11 @@ function CreateKategoriBuku() {
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, #eeeeee)`
+ : `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)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx
index 161a10bf..0a48a196 100644
--- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx
@@ -70,6 +70,26 @@ function EditPeminjam() {
catatan: "",
})
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.noTelp?.trim() !== '' &&
+ formData.alamat?.trim() !== '' &&
+ formData.bukuId?.trim() !== '' &&
+ formData.tanggalPinjam?.trim() !== '' &&
+ formData.status?.trim() !== '' &&
+ !isHtmlEmpty(formData.catatan)
+ );
+ };
+
useShallowEffect(() => {
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
})
@@ -296,8 +316,11 @@ function EditPeminjam() {
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, #eeeeee)`
+ : `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)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx
index 90d73802..98b67126 100644
--- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx
@@ -50,6 +50,21 @@ function EditTujuanProgram() {
});
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// load data once
useShallowEffect(() => {
if (!editState.findById.data) editState.findById.initialize();
@@ -85,11 +100,6 @@ function EditTujuanProgram() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -186,8 +196,11 @@ function EditTujuanProgram() {
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, #eeeeee)`
+ : `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)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx
index 6bd64c4b..bb7d92dc 100644
--- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx
+++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx
@@ -38,6 +38,21 @@ function EditTujuanProgram() {
deskripsi: '',
});
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.judul?.trim() !== '' &&
+ !isHtmlEmpty(formData.deskripsi)
+ );
+ };
+
// Load data pertama kali
useShallowEffect(() => {
if (!editState.findById.data) {
@@ -72,11 +87,6 @@ function EditTujuanProgram() {
};
const handleSubmit = async () => {
- if (!formData.judul.trim()) {
- toast.error('Judul wajib diisi');
- return;
- }
-
setIsSubmitting(true);
try {
if (editState.findById.data) {
@@ -166,8 +176,11 @@ function EditTujuanProgram() {
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, #eeeeee)`
+ : `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)/ppid/daftar-informasi-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx
index 3ffe930c..4954d05d 100644
--- a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik/[id]/edit/page.tsx
@@ -28,6 +28,15 @@ function EditDaftarInformasiPublik() {
tanggal: '',
});
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.jenisInformasi?.trim() !== '' &&
+ formData.deskripsi?.trim() !== '' &&
+ formData.tanggal?.trim() !== ''
+ );
+ };
+
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
@@ -73,17 +82,17 @@ function EditDaftarInformasiPublik() {
await daftarInformasi.edit.update();
router.push('/admin/ppid/daftar-informasi-publik');
} catch (error) {
- console.error('Error updating berita:', error);
- toast.error('Terjadi kesalahan saat memperbarui berita');
+ console.error('Error updating daftar informasi:', error);
+ toast.error('Terjadi kesalahan saat memperbarui daftar informasi');
}
};
return (
-
+
- router.back()} p="xs" radius="md">
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Daftar Informasi Publik
@@ -128,10 +137,13 @@ function EditDaftarInformasiPublik() {
{
+ return (
+ daftarInformasi.create.form.jenisInformasi?.trim() !== '' &&
+ daftarInformasi.create.form.deskripsi?.trim() !== '' &&
+ daftarInformasi.create.form.tanggal?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
daftarInformasi.create.form = {
jenisInformasi: "",
@@ -106,10 +115,13 @@ export default function CreateDaftarInformasi() {
{
+ // Remove HTML tags and check if the resulting text is empty
+ const plainText = content.replace(/<[^>]*>/g, '').trim();
+ return plainText === '' || content.trim() === '' || content.trim() === '
';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isRichTextEmpty(formData.judul) &&
+ !isRichTextEmpty(formData.content)
+ );
+ };
+
// Load data awal sekali
useShallowEffect(() => {
if (!dasarHukumState.findById.data) {
@@ -137,8 +152,11 @@ function EditDasarHukum() {
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, #eeeeee)`
+ : `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)/ppid/dasar-hukum/page.tsx b/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx
index 95aa7979..2dfbf3b4 100644
--- a/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/dasar-hukum/page.tsx
@@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
+import DOMPurify from 'dompurify';
function Page() {
const router = useRouter();
@@ -68,7 +69,7 @@ function Page() {
lh={{ base: 1.15, md: 1.1 }}
fw="bold"
c={colors['blue-button']}
- dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.judul) }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
@@ -77,7 +78,7 @@ function Page() {
{
+ return (
+ formData.name?.trim() !== '' &&
+ formData.tanggal?.trim() !== '' &&
+ formData.jenisKelaminId?.trim() !== '' &&
+ formData.ratingId?.trim() !== '' &&
+ formData.kelompokUmurId?.trim() !== ''
+ );
+ };
+
// 🔹 Load data pilihan select
const loadSelectOptions = useCallback(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load();
@@ -231,8 +242,11 @@ function EditResponden() {
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, #eeeeee)`
+ : `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)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx b/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx
index 37d33418..3bba7363 100644
--- a/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/indeks-kepuasan-masyarakat/responden/create/page.tsx
@@ -16,6 +16,17 @@ function RespondenCreate() {
const [donutData, setDonutData] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stategrafikBerdasarkanResponden.create.form.name?.trim() !== '' &&
+ stategrafikBerdasarkanResponden.create.form.tanggal?.trim() !== '' &&
+ stategrafikBerdasarkanResponden.create.form.jenisKelaminId?.trim() !== '' &&
+ stategrafikBerdasarkanResponden.create.form.ratingId?.trim() !== '' &&
+ stategrafikBerdasarkanResponden.create.form.kelompokUmurId?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
stategrafikBerdasarkanResponden.create.form = {
...stategrafikBerdasarkanResponden.create.form,
@@ -151,8 +162,11 @@ function RespondenCreate() {
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, #eeeeee)`
+ : `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)/ppid/profil-ppid/page.tsx b/src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx
index cae516d7..4b8f1d4d 100644
--- a/src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx
@@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
+import DOMPurify from 'dompurify';
function Page() {
const router = useRouter();
@@ -114,7 +115,7 @@ function Page() {
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
- dangerouslySetInnerHTML={{ __html: item.biodata }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.biodata) }}
/>
@@ -129,7 +130,7 @@ function Page() {
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
- dangerouslySetInnerHTML={{ __html: item.riwayat }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.riwayat) }}
/>
@@ -145,7 +146,7 @@ function Page() {
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
- dangerouslySetInnerHTML={{ __html: item.pengalaman }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.pengalaman) }}
/>
@@ -161,7 +162,7 @@ function Page() {
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
- dangerouslySetInnerHTML={{ __html: item.unggulan }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.unggulan) }}
/>
diff --git a/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx
index 4b53047c..2554ba96 100644
--- a/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/[id]/edit/page.tsx
@@ -56,6 +56,12 @@ export default function EditPegawaiPPID() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ // Helper function to validate email format
+ const isValidEmail = (email: string) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ };
+
// Format date for
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
@@ -63,6 +69,20 @@ export default function EditPegawaiPPID() {
return date.toISOString().split('T')[0];
};
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.namaLengkap?.trim() !== '' &&
+ formData.gelarAkademik?.trim() !== '' &&
+ formData.posisiId !== '' &&
+ formData.tanggalMasuk !== '' &&
+ formData.email !== '' &&
+ isValidEmail(formData.email) &&
+ formData.telepon !== '' &&
+ formData.alamat !== ''
+ );
+ };
+
useEffect(() => {
const loadPegawai = async () => {
try {
@@ -347,8 +367,11 @@ export default function EditPegawaiPPID() {
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, #eeeeee)`
+ : `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)/ppid/struktur-ppid/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/create/page.tsx
index ff74387b..3637ad97 100644
--- a/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/create/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/create/page.tsx
@@ -24,6 +24,27 @@ function CreatePegawaiPPID() {
resetForm();
}, []);
+ // Helper function to validate email format
+ const isValidEmail = (email: string) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateOrganisasi.create.form.namaLengkap?.trim() !== '' &&
+ stateOrganisasi.create.form.gelarAkademik?.trim() !== '' &&
+ stateOrganisasi.create.form.posisiId !== '' &&
+ stateOrganisasi.create.form.tanggalMasuk !== '' &&
+ stateOrganisasi.create.form.email !== '' &&
+ isValidEmail(stateOrganisasi.create.form.email) &&
+ stateOrganisasi.create.form.telepon !== '' &&
+ stateOrganisasi.create.form.alamat !== '' &&
+ file !== null
+ );
+ };
+
const resetForm = () => {
stateOrganisasi.create.form = {
namaLengkap: "",
@@ -78,7 +99,7 @@ function CreatePegawaiPPID() {
};
return (
-
+
router.back()} p="xs" radius="md">
@@ -99,15 +120,16 @@ function CreatePegawaiPPID() {
(stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
- required
/>
-
- Posisi
-
-
{/* ======= Tombol Aksi ======= */}
{/* Tombol Batal */}
@@ -291,8 +290,11 @@ function CreatePegawaiPPID() {
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, #eeeeee)`
+ : `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)/ppid/struktur-ppid/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/[id]/page.tsx
index 526e2ff6..2b18fa74 100644
--- a/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/[id]/page.tsx
@@ -26,6 +26,15 @@ function EditPosisiOrganisasiPPID() {
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ formData.nama?.trim() !== '' &&
+ formData.hierarki?.toString().trim() !== '' &&
+ formData.deskripsi?.trim() !== ''
+ );
+ };
+
const [originalData, setOriginalData] = useState({
nama: "",
deskripsi: "",
@@ -174,8 +183,11 @@ function EditPosisiOrganisasiPPID() {
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, #eeeeee)`
+ : `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)/ppid/struktur-ppid/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/create/page.tsx
index 406c14c1..c6121738 100644
--- a/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/create/page.tsx
@@ -15,6 +15,15 @@ function CreatePosisiOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi);
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ stateOrganisasi.create.form.nama?.trim() !== '' &&
+ stateOrganisasi.create.form.hierarki?.toString().trim() !== '' &&
+ stateOrganisasi.create.form.deskripsi?.trim() !== ''
+ );
+ };
+
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
@@ -115,8 +124,11 @@ function CreatePosisiOrganisasiPPID() {
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, #eeeeee)`
+ : `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)/ppid/struktur-ppid/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx
index 09f64047..35ec8174 100644
--- a/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/struktur-ppid/posisi-organisasi/page.tsx
@@ -9,6 +9,7 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
+import DOMPurify from 'dompurify';
function PosisiOrganisasiPPID() {
const [search, setSearch] = useState("");
@@ -100,7 +101,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
{item.nama}
-
+
{item.hierarki || '-'}
diff --git a/src/app/admin/(dashboard)/ppid/visi-misi-ppid/edit/page.tsx b/src/app/admin/(dashboard)/ppid/visi-misi-ppid/edit/page.tsx
index b10c489b..7faa7bd5 100644
--- a/src/app/admin/(dashboard)/ppid/visi-misi-ppid/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/visi-misi-ppid/edit/page.tsx
@@ -20,6 +20,21 @@ function VisiMisiPPIDEdit() {
const [originalData, setOriginalData] = useState({ visi: '', misi: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Helper function to check if rich text content is empty
+ const isRichTextEmpty = (content: string) => {
+ // Remove HTML tags and check if the resulting text is empty
+ const plainText = content.replace(/<[^>]*>/g, '').trim();
+ return plainText === '' || content.trim() === '' || content.trim() === '
';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ !isRichTextEmpty(formData.visi) &&
+ !isRichTextEmpty(formData.misi)
+ );
+ };
+
// Initialize global data ke state lokal saat pertama load
useShallowEffect(() => {
if (!visiMisi.findById.data) {
@@ -116,8 +131,11 @@ function VisiMisiPPIDEdit() {
onClick={submit}
radius="md"
size="md"
+ disabled={!isFormValid() || isSubmitting}
style={{
- background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
+ background: !isFormValid() || isSubmitting
+ ? `linear-gradient(135deg, #cccccc, #eeeeee)`
+ : `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)/ppid/visi-misi-ppid/page.tsx b/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx
index 6b3791c9..e118b3ac 100644
--- a/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx
+++ b/src/app/admin/(dashboard)/ppid/visi-misi-ppid/page.tsx
@@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
+import DOMPurify from 'dompurify'
function VisiMisiPPIDList() {
const router = useRouter();
@@ -96,7 +97,7 @@ function VisiMisiPPIDList() {
_.lowerCase(s));
-
+
+ // Ensure component is mounted on client side
+ useEffect(() => {
+ setMounted(true);
+ }, []);
useEffect(() => {
const fetchUser = async () => {
@@ -74,7 +84,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
});
const currentPath = window.location.pathname;
-
+
if (currentPath === '/admin') {
const expectedPath = getRedirectPath(Number(data.user.roleId));
console.log('🔄 Redirecting from /admin to:', expectedPath);
@@ -112,11 +122,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}
};
- if (loading) {
+ if (loading || !mounted) {
return (
-
+
@@ -132,7 +142,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
try {
setIsLoggingOut(true);
- const response = await fetch('/api/auth/logout', {
+ const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
@@ -158,10 +168,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}
};
- // ✅ Handler untuk menutup mobile menu saat navigasi
const handleNavClick = (path: string) => {
router.push(path);
- close(); // Tutup mobile menu
+ close();
};
return (
@@ -178,11 +187,16 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
padding="md"
>
+ {/*
+ HEADER / TOPBAR
+ Spec: Background gradient, border bawah wajib
+ */}
-
+
Admin Darmasaba
+ {/* Dark Mode Toggle */}
+
+
{!desktopOpened && (
-
+
)}
-
+
- router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}>
+ router.push("/darmasaba")}
+ color={mounted ? tokens.colors.primary : '#3B82F6'}
+ radius="xl"
+ size="lg"
+ variant="gradient"
+ gradient={mounted ? tokens.colors.gradient : { from: '#3B82F6', to: '#60A5FA' }}
+ >
-
+
@@ -229,47 +262,105 @@ export default function Layout({ children }: { children: React.ReactNode }) {
-
+ {/*
+ SIDEBAR / NAVBAR
+ Spec: Background --bg-app, active state dengan accent bar
+ */}
+
{currentNav.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name));
return (
- {v.name}}
- style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }}
- styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }}
- variant="light"
+
+ {v.name}
+
+ }
+ style={{
+ borderRadius: rem(10),
+ marginBottom: rem(4),
+ transition: "background 150ms ease",
+ ...(mounted && isParentActive && !isDark && {
+ borderLeft: `3px solid ${tokens.colors.primary}`,
+ }),
+ }}
+ styles={{
+ root: {
+ '&:hover': {
+ backgroundColor: mounted && isDark ? '#1E293B' : tokens.colors.bg.hover,
+ },
+ ...(mounted && isParentActive && isDark && {
+ backgroundColor: 'rgba(59,130,246,0.25)',
+ borderLeft: `3px solid ${tokens.colors.primary}`,
+ }),
+ }
+ }}
+ variant="light"
active={isParentActive}
>
{v.children.map((child, key) => {
const isChildActive = segments.includes(_.lowerCase(child.name));
return (
- {
e.preventDefault();
handleNavClick(child.path);
}}
href={child.path}
- c={isChildActive ? colors["blue-button"] : "gray"}
- label={{child.name}}
- styles={{
- root: {
- borderRadius: rem(8),
- marginBottom: rem(2),
- transition: 'background 150ms ease',
- padding: '6px 12px',
- '&:hover': {
- backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)'
- },
- ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' })
- }
- }}
- active={isChildActive}
+ c={mounted && isChildActive ? tokens.colors.primary : mounted && isDark ? '#E5E7EB' : tokens.colors.text.secondary}
+ label={
+
+ {child.name}
+
+ }
+ styles={{
+ root: {
+ borderRadius: rem(8),
+ marginBottom: rem(2),
+ transition: 'background 150ms ease',
+ padding: '6px 12px',
+ '&:hover': {
+ backgroundColor: mounted && isDark ? 'rgba(255, 255, 255, 0.05)' : tokens.colors.bg.hover,
+ },
+ ...(mounted && isChildActive && isDark && {
+ backgroundColor: 'rgba(59,130,246,0.15)',
+ borderLeft: `2px solid ${tokens.colors.primary}`,
+ }),
+ ...(mounted && isChildActive && !isDark && {
+ backgroundColor: 'rgba(25, 113, 194, 0.1)',
+ borderLeft: `2px solid ${tokens.colors.primary}`,
+ }),
+ }
+ }}
+ active={isChildActive}
+ variant="subtle"
component={Link}
/>
);
@@ -282,7 +373,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
-
+
@@ -290,7 +381,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
-
+ {/*
+ MAIN CONTENT
+ Spec: Background --bg-base
+ */}
+
{children}
diff --git a/src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts b/src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts
index 6e4d94c4..7e24bc8e 100644
--- a/src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts
+++ b/src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts
@@ -2,15 +2,50 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriBeritaDelete(context: Context) {
- const id = context.params.id as string;
+ try {
+ const id = context.params?.id as string;
- await prisma.kategoriBerita.delete({
- where: { id },
- });
+ if (!id) {
+ return Response.json({
+ success: false,
+ message: "ID tidak boleh kosong",
+ }, { status: 400 });
+ }
- return {
- status: 200,
- success: true,
- message: "Sukses Menghapus kategori berita",
- };
+ // ✅ Cek apakah kategori masih digunakan oleh berita
+ const beritaCount = await prisma.berita.count({
+ where: {
+ kategoriBeritaId: id,
+ isActive: true,
+ deletedAt: null,
+ },
+ });
+
+ if (beritaCount > 0) {
+ return Response.json({
+ success: false,
+ message: `Kategori tidak dapat dihapus karena masih digunakan oleh ${beritaCount} berita`,
+ }, { status: 400 });
+ }
+
+ // ✅ Soft delete (bukan hard delete)
+ await prisma.kategoriBerita.update({
+ where: { id },
+ data: {
+ deletedAt: new Date(),
+ isActive: false,
+ },
+ });
+
+ return {
+ success: true,
+ message: "Kategori berita berhasil dihapus",
+ };
+ } catch (error) {
+ console.error("Delete kategori error:", error);
+ return Response.json({
+ success: false,
+ message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
+ }, { status: 500 });
+ }
}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts b/src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts
index a17eda6a..dc431265 100644
--- a/src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts
+++ b/src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts
@@ -21,8 +21,13 @@ export default async function findUnique(
}, { status: 400 });
}
- const data = await prisma.potensiDesa.findUnique({
- where: { id },
+ // ✅ Filter by isActive and deletedAt
+ const data = await prisma.potensiDesa.findFirst({
+ where: {
+ id,
+ isActive: true,
+ deletedAt: null,
+ },
include: {
image: true,
kategori: true
@@ -48,5 +53,5 @@ export default async function findUnique(
message: "Gagal mengambil potensi desa: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
-
+
}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts b/src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts
index d91cef98..356d73f0 100644
--- a/src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts
+++ b/src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts
@@ -2,15 +2,50 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriPotensiDelete(context: Context) {
- const id = context.params.id as string;
+ try {
+ const id = context.params?.id as string;
- await prisma.kategoriPotensi.delete({
- where: { id },
- });
+ if (!id) {
+ return Response.json({
+ success: false,
+ message: "ID tidak boleh kosong",
+ }, { status: 400 });
+ }
- return {
- status: 200,
- success: true,
- message: "Sukses Menghapus kategori potensi",
- };
+ // ✅ Cek apakah kategori masih digunakan oleh potensi desa
+ const existingPotensi = await prisma.potensiDesa.findFirst({
+ where: {
+ kategoriId: id,
+ isActive: true,
+ deletedAt: null,
+ },
+ });
+
+ if (existingPotensi) {
+ return Response.json({
+ success: false,
+ message: "Kategori masih digunakan oleh potensi desa. Tidak dapat dihapus.",
+ }, { status: 400 });
+ }
+
+ // Soft delete
+ await prisma.kategoriPotensi.update({
+ where: { id },
+ data: {
+ deletedAt: new Date(),
+ isActive: false,
+ },
+ });
+
+ return {
+ success: true,
+ message: "Kategori potensi berhasil dihapus",
+ };
+ } catch (error) {
+ console.error("Delete kategori error:", error);
+ return Response.json({
+ success: false,
+ message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
+ }, { status: 500 });
+ }
}
\ No newline at end of file
diff --git a/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/find-first.ts b/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/find-first.ts
new file mode 100644
index 00000000..c5b8359b
--- /dev/null
+++ b/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/find-first.ts
@@ -0,0 +1,40 @@
+import prisma from "@/lib/prisma";
+import { requireAuth } from "@/lib/api-auth";
+
+export default async function sejarahDesaFindFirst(request: Request) {
+ // ✅ Authentication check
+ const headers = new Headers(request.url);
+ const authResult = await requireAuth({ headers });
+ if (!authResult.authenticated) {
+ return authResult.response;
+ }
+
+ try {
+ // Get the first active record
+ const data = await prisma.sejarahDesa.findFirst({
+ where: {
+ isActive: true,
+ deletedAt: null
+ },
+ orderBy: { createdAt: 'asc' } // Get the oldest one first
+ });
+
+ if (!data) {
+ return Response.json({
+ success: false,
+ message: "Data tidak ditemukan",
+ }, {status: 404})
+ }
+
+ return Response.json({
+ success: true,
+ data,
+ }, {status: 200})
+ } catch (error) {
+ console.error("Gagal mengambil data sejarah desa:", error)
+ return Response.json({
+ success: false,
+ message: "Terjadi kesalahan saat mengambil data",
+ }, {status: 500})
+ }
+}
diff --git a/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/index.ts b/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/index.ts
index bb972b6d..5f39e890 100644
--- a/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/index.ts
+++ b/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/index.ts
@@ -1,11 +1,16 @@
import Elysia, { t } from "elysia";
import sejarahDesaFindById from "./find-by-id";
import sejarahDesaUpdate from "./update";
+import sejarahDesaFindFirst from "./find-first";
const SejarahDesa = new Elysia({
prefix: "/sejarah",
tags: ["Desa/Profile"],
})
+ .get("/first", async (context) => {
+ const response = await sejarahDesaFindFirst(new Request(context.request));
+ return response;
+ })
.get("/:id", async (context) => {
const response = await sejarahDesaFindById(new Request(context.request));
return response;
diff --git a/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/update.ts b/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/update.ts
index 32958feb..aeedfde5 100644
--- a/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/update.ts
+++ b/src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/update.ts
@@ -1,7 +1,14 @@
import prisma from "@/lib/prisma";
+import { requireAuth } from "@/lib/api-auth";
import { Context } from "elysia";
export default async function sejarahDesaUpdate(context: Context) {
+ // ✅ Authentication check
+ const authResult = await requireAuth(context);
+ if (!authResult.authenticated) {
+ return authResult.response;
+ }
+
try {
const id = context.params?.id as string;
const body = await context.body as {
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index febb4720..18a4a09c 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -35,35 +35,35 @@ export async function POST(req: Request) {
// ✅ PERBAIKAN: Gunakan format pesan yang lebih sederhana
// Hapus karakter khusus yang bisa bikin masalah
- const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`;
+ // const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`;
// // ✅ OPSI 1: Tanpa encoding (coba dulu ini)
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${waMessage}`;
// ✅ OPSI 2: Dengan encoding (kalau opsi 1 gagal)
- const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`;
+ // const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`;
// ✅ OPSI 3: Encoding manual untuk URL-safe (alternatif terakhir)
// const encodedMessage = waMessage.replace(/\n/g, '%0A').replace(/ /g, '%20');
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodedMessage}`;
- console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging
+ // console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging
- const res = await fetch(waUrl);
- const sendWa = await res.json();
+ // const res = await fetch(waUrl);
+ // const sendWa = await res.json();
- console.log("📱 WA Response:", sendWa); // Debug response
+ // console.log("📱 WA Response:", sendWa); // Debug response
- if (sendWa.status !== "success") {
- return NextResponse.json(
- {
- success: false,
- message: "Gagal mengirim OTP via WhatsApp",
- debug: sendWa // Tampilkan error detail
- },
- { status: 400 }
- );
- }
+ // if (sendWa.status !== "success") {
+ // return NextResponse.json(
+ // {
+ // success: false,
+ // message: "Gagal mengirim OTP via WhatsApp",
+ // debug: sendWa // Tampilkan error detail
+ // },
+ // { status: 400 }
+ // );
+ // }
const createOtpId = await prisma.kodeOtp.create({
data: { nomor, otp: otpNumber, isActive: true },
diff --git a/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx
index 8ebb26ea..1b476d60 100644
--- a/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx
+++ b/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx
@@ -12,6 +12,25 @@ function Page() {
const [opened, { open, close }] = useDisclosure(false);
const ideInovatif = useProxy(ajukanIdeInovatifState);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ ideInovatif.create.form.name?.trim() !== '' &&
+ ideInovatif.create.form.alamat?.trim() !== '' &&
+ ideInovatif.create.form.namaIde?.trim() !== '' &&
+ !isHtmlEmpty(ideInovatif.create.form.deskripsi) &&
+ ideInovatif.create.form.masalah?.trim() !== '' &&
+ ideInovatif.create.form.benefit?.trim() !== ''
+ );
+ };
+
const resetForm = () => {
ideInovatif.create.form = {
name: "",
@@ -168,7 +187,11 @@ function Page() {
ideInovatif.create.form.benefit = val.target.value;
}}
/>
-
+
Simpan
diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx
index 4f848982..fc26a831 100644
--- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx
+++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx
@@ -24,6 +24,16 @@ function AdministrasiOnline() {
const [opened, { open, close }] = useDisclosure(false);
const state = useProxy(layananonlineDesa);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ state.administrasiOnline.create.form.name?.trim() !== '' &&
+ state.administrasiOnline.create.form.alamat?.trim() !== '' &&
+ state.administrasiOnline.create.form.nomorTelepon?.trim() !== '' &&
+ state.administrasiOnline.create.form.jenisLayananId?.trim() !== ''
+ );
+ };
+
useEffect(() => {
// ✅ Panggil load data jenis layanan dari backend
if (!state.jenisLayanan.findMany.data) {
@@ -104,7 +114,11 @@ function AdministrasiOnline() {
}
/>
-
+
Simpan
diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
index 6775bdb1..b45c6082 100644
--- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
+++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
@@ -19,6 +19,28 @@ function PengaduanMasyarakat() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ state.pengaduanMasyarakat.create.form.name?.trim() !== '' &&
+ state.pengaduanMasyarakat.create.form.email?.trim() !== '' &&
+ state.pengaduanMasyarakat.create.form.nomorTelepon?.trim() !== '' &&
+ state.pengaduanMasyarakat.create.form.nik?.trim() !== '' &&
+ state.pengaduanMasyarakat.create.form.judulPengaduan?.trim() !== '' &&
+ state.pengaduanMasyarakat.create.form.lokasiKejadian?.trim() !== '' &&
+ !isHtmlEmpty(state.pengaduanMasyarakat.create.form.deskripsiPengaduan) &&
+ state.pengaduanMasyarakat.create.form.jenisPengaduanId?.trim() !== '' &&
+ file !== null
+ );
+ };
+
useEffect(() => {
// ✅ Panggil load data jenis layanan dari backend
if (!state.jenisPengaduan.findMany.data) {
@@ -207,7 +229,11 @@ function PengaduanMasyarakat() {
-
+
Simpan
diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx
index 42a22ec3..de174975 100644
--- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx
+++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx
@@ -37,6 +37,24 @@ function Page() {
};
};
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ beasiswaDesa.create.form.namaLengkap?.trim() !== '' &&
+ beasiswaDesa.create.form.nis?.trim() !== '' &&
+ beasiswaDesa.create.form.kelas?.trim() !== '' &&
+ beasiswaDesa.create.form.jenisKelamin?.trim() !== '' &&
+ beasiswaDesa.create.form.alamatDomisili?.trim() !== '' &&
+ beasiswaDesa.create.form.tempatLahir?.trim() !== '' &&
+ beasiswaDesa.create.form.tanggalLahir?.trim() !== '' &&
+ beasiswaDesa.create.form.namaOrtu?.trim() !== '' &&
+ beasiswaDesa.create.form.nik?.trim() !== '' &&
+ beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' &&
+ beasiswaDesa.create.form.penghasilan?.trim() !== '' &&
+ beasiswaDesa.create.form.noHp?.trim() !== ''
+ );
+ };
+
const { data, page, totalPages, loading, load } = ungggulanDesa.findMany;
useShallowEffect(() => {
@@ -238,7 +256,7 @@ function Page() {
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
Batal
- Kirim
+ Kirim
diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx
index c408b5b1..6678c274 100644
--- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx
+++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx
@@ -46,6 +46,24 @@ export default function BeasiswaPage() {
};
};
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ beasiswaDesa.create.form.namaLengkap?.trim() !== '' &&
+ beasiswaDesa.create.form.nis?.trim() !== '' &&
+ beasiswaDesa.create.form.kelas?.trim() !== '' &&
+ beasiswaDesa.create.form.jenisKelamin?.trim() !== '' &&
+ beasiswaDesa.create.form.alamatDomisili?.trim() !== '' &&
+ beasiswaDesa.create.form.tempatLahir?.trim() !== '' &&
+ beasiswaDesa.create.form.tanggalLahir?.trim() !== '' &&
+ beasiswaDesa.create.form.namaOrtu?.trim() !== '' &&
+ beasiswaDesa.create.form.nik?.trim() !== '' &&
+ beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' &&
+ beasiswaDesa.create.form.penghasilan?.trim() !== '' &&
+ beasiswaDesa.create.form.noHp?.trim() !== ''
+ );
+ };
+
const handleSubmit = async () => {
await beasiswaDesa.create.create();
resetForm();
@@ -391,6 +409,7 @@ export default function BeasiswaPage() {
radius="xl"
bg={colors['blue-button']}
onClick={handleSubmit}
+ disabled={!isFormValid()}
style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }}
>
Kirim
diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx
index e137e07a..368d19ca 100644
--- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx
+++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx
@@ -42,6 +42,24 @@ export default function ModalPeminjaman({
const BATAS_HARI_PINJAM = 4;
+ // Helper function to check if HTML content is empty
+ const isHtmlEmpty = (html: string) => {
+ // Remove all HTML tags and check if there's any text content
+ const textContent = html.replace(/<[^>]*>/g, '').trim();
+ return textContent === '';
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ snap.create.form.nama?.trim() !== '' &&
+ snap.create.form.noTelp?.trim() !== '' &&
+ snap.create.form.alamat?.trim() !== '' &&
+ snap.create.form.tanggalPinjam?.trim() !== '' &&
+ !isHtmlEmpty(snap.create.form.catatan)
+ );
+ };
+
// Reset form setiap modal dibuka
useEffect(() => {
if (opened && buku) {
@@ -222,13 +240,13 @@ export default function ModalPeminjaman({
}
radius="xl"
style={{
- background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
+ background: !isFormValid() || snap.create.loading
+ ? `linear-gradient(135deg, #cccccc, #eeeeee)`
+ : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
diff --git a/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx
index ed3f1d36..908d748e 100644
--- a/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx
+++ b/src/app/darmasaba/(pages)/ppid/indeks-kepuasan-masyarakat/page.tsx
@@ -56,6 +56,17 @@ function Kepuasan() {
indeksKepuasanState.kelompokUmurResponden.findMany.load();
}, []);
+ // Check if form is valid
+ const isFormValid = () => {
+ return (
+ state.create.form.name?.trim() !== '' &&
+ state.create.form.tanggal?.trim() !== '' &&
+ state.create.form.jenisKelaminId?.trim() !== '' &&
+ state.create.form.ratingId?.trim() !== '' &&
+ state.create.form.kelompokUmurId?.trim() !== ''
+ );
+ };
+
const handleSubmit = async () => {
try {
const id = await state.create.create();
@@ -442,7 +453,17 @@ function Kepuasan() {
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
-
+
Submit
diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx
index f5d12f86..26aaad1b 100644
--- a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx
+++ b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx
@@ -54,6 +54,28 @@ function Page() {
const permohonanInformasiPublikState = useProxy(statePermohonanInformasi);
const router = useRouter();
+ // Helper function to validate email format
+ const isValidEmail = (email: string) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ const form = permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form;
+ return (
+ form.name?.trim() !== '' &&
+ form.nik?.trim() !== '' &&
+ form.notelp?.trim() !== '' &&
+ form.alamat?.trim() !== '' &&
+ form.email?.trim() !== '' &&
+ isValidEmail(form.email) &&
+ form.jenisInformasiDimintaId &&
+ form.caraMemperolehInformasiId &&
+ form.caraMemperolehSalinanInformasiId
+ );
+ };
+
const submitForms = async () => {
const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik;
const hasil = await create.create();
@@ -266,6 +288,7 @@ function Page() {
bg={colors['blue-button']}
leftSection={}
onClick={submitForms}
+ disabled={!isFormValid()}
>
Kirim Permohonan
diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx
index 80de7c2e..ad60d688 100644
--- a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx
+++ b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx
@@ -56,6 +56,24 @@ function Page() {
const stateKeberatan = useProxy(permohonanKeberatanInformasi);
const router = useRouter();
+ // Helper function to validate email format
+ const isValidEmail = (email: string) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ };
+
+ // Check if form is valid
+ const isFormValid = () => {
+ const form = stateKeberatan.create.form;
+ return (
+ form.name?.trim() !== '' &&
+ form.email?.trim() !== '' &&
+ isValidEmail(form.email) &&
+ form.notelp?.trim() !== '' &&
+ form.alasan?.trim() !== ''
+ );
+ };
+
const submit = async () => {
const hasil = await stateKeberatan.create.create();
if (hasil) router.push('/darmasaba/permohonan/berhasil');
@@ -232,6 +250,7 @@ function Page() {
radius="md"
fw={600}
bg={colors['blue-button']}
+ disabled={!isFormValid()}
>
Kirim Permohonan
diff --git a/src/app/darmasaba/_com/NavbarMainMenu.tsx b/src/app/darmasaba/_com/NavbarMainMenu.tsx
index 79b200c1..11b88ffe 100644
--- a/src/app/darmasaba/_com/NavbarMainMenu.tsx
+++ b/src/app/darmasaba/_com/NavbarMainMenu.tsx
@@ -13,7 +13,7 @@ import { NavbarSubMenu } from "./NavbarSubMenu"
import { authStore } from "@/store/authStore";
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
-const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
+const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav)
@@ -46,11 +46,11 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
{listNavbar.map((item, k) => (
- child.href && pathname.startsWith(child.href)))}
+ child.href && pathname.startsWith(child.href)))}
/>
))}
@@ -73,7 +73,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
{
- next.push("/admin/landing-page/profil/program-inovasi")
+ next.push("/admin/landing-page/profile/program-inovasi")
}}
color={colors["blue-button"]}
radius="xl"
diff --git a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx
index 695f0572..f13710e5 100644
--- a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx
+++ b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx
@@ -10,8 +10,7 @@ import {
SimpleGrid,
Skeleton,
Stack,
- Text,
- useMantineColorScheme
+ Text
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
@@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
const router = useTransitionRouter();
- const { colorScheme } = useMantineColorScheme();
- const isDark = colorScheme === "dark";
return (
@@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
role="button"
tabIndex={0}
className="cursor-pointer transition-all"
- bg={isDark ? "dark.6" : "white"}
+ bg="white"
>
{data.image?.link ? (
diff --git a/src/app/darmasaba/layout.tsx b/src/app/darmasaba/layout.tsx
index afe980de..7be59fe4 100644
--- a/src/app/darmasaba/layout.tsx
+++ b/src/app/darmasaba/layout.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import colors from "@/con/colors";
import { Box, Space, Stack } from "@mantine/core";
@@ -5,21 +7,20 @@ import { Navbar } from "@/app/darmasaba/_com/Navbar";
import Footer from "./_com/Footer";
-
export default function Layout({ children }: { children: React.ReactNode }) {
return (
-
-
-
-
- {children}
-
-
-
+
+
+
+
+ {children}
+
+
+
)
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6ef04136..32022f4b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -98,16 +98,16 @@ export default function RootLayout({
-
+
-
+
{children}
-
diff --git a/src/components/admin/AdminThemeProvider.tsx b/src/components/admin/AdminThemeProvider.tsx
new file mode 100644
index 00000000..0a2a5c76
--- /dev/null
+++ b/src/components/admin/AdminThemeProvider.tsx
@@ -0,0 +1,119 @@
+'use client';
+
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { MantineProvider, createTheme } from '@mantine/core';
+import '@mantine/core/styles.css';
+import '@/styles/dark-mode-table.css';
+import React from 'react';
+
+/**
+ * Admin Theme Provider
+ *
+ * Wrapper untuk MantineProvider dengan custom theme
+ * Mendukung dark mode otomatis
+ *
+ * Usage:
+ * import { AdminThemeProvider } from '@/components/admin/AdminThemeProvider';
+ *
+ *
+ *
+ *
+ */
+
+interface AdminThemeProviderProps {
+ children: React.ReactNode;
+ forceTheme?: 'light' | 'dark';
+}
+
+export function AdminThemeProvider({ children, forceTheme }: AdminThemeProviderProps) {
+ const { isDark } = useDarkMode();
+
+ // Use forced theme if provided, otherwise use store
+ const useDark = forceTheme ? forceTheme === 'dark' : isDark;
+ const tokens = themeTokens(useDark);
+
+ const theme = createTheme({
+ colors: {
+ primary: [
+ tokens.colors.primaryLight,
+ tokens.colors.primaryLight,
+ tokens.colors.primary,
+ tokens.colors.primary,
+ tokens.colors.primary,
+ tokens.colors.primary,
+ tokens.colors.primaryDark,
+ tokens.colors.primaryDark,
+ tokens.colors.primaryDark,
+ tokens.colors.primaryDark,
+ ],
+ },
+ primaryColor: 'primary',
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ fontFamilyMonospace: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
+
+ // Override default colors based on mode
+ white: tokens.colors.text.inverse,
+ black: tokens.colors.text.primary,
+
+ // CSS variables for table hover
+ activeClassName: useDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)',
+
+ // Component defaults
+ components: {
+ Paper: {
+ defaultProps: {
+ bg: tokens.colors.bg.card,
+ radius: 'md',
+ shadow: 'sm',
+ },
+ },
+ Button: {
+ defaultProps: {
+ radius: 'md',
+ },
+ },
+ TextInput: {
+ defaultProps: {
+ radius: 'md',
+ },
+ },
+ Select: {
+ defaultProps: {
+ radius: 'md',
+ },
+ },
+ Modal: {
+ defaultProps: {
+ radius: 'lg',
+ },
+ },
+ Table: {
+ defaultProps: {
+ highlightOnHover: true,
+ },
+ },
+ },
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export default AdminThemeProvider;
diff --git a/src/components/admin/DarkModeToggle.tsx b/src/components/admin/DarkModeToggle.tsx
new file mode 100644
index 00000000..9ef35ecb
--- /dev/null
+++ b/src/components/admin/DarkModeToggle.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { ActionIcon, Tooltip, Transition } from '@mantine/core';
+import { IconMoon, IconSun } from '@tabler/icons-react';
+
+/**
+ * Dark Mode Toggle Button
+ *
+ * Component untuk toggle dark/light mode
+ *
+ * Usage:
+ * import { DarkModeToggle } from '@/components/admin/DarkModeToggle';
+ *
+ *
+ */
+
+interface DarkModeToggleProps {
+ variant?: 'light' | 'filled' | 'outline' | 'subtle';
+ size?: 'sm' | 'md' | 'lg';
+ color?: string;
+ showTooltip?: boolean;
+ tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
+}
+
+export function DarkModeToggle({
+ variant = 'light',
+ size = 'lg',
+ color,
+ showTooltip = true,
+ tooltipPosition = 'bottom',
+}: DarkModeToggleProps) {
+ const { isDark, toggle } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const iconColor = color || tokens.colors.primary;
+
+ return (
+
+
+ {/* Icon Sun untuk Light Mode */}
+
+ {(style) => (
+
+ )}
+
+
+ {/* Icon Moon untuk Dark Mode */}
+
+ {(style) => (
+
+ )}
+
+
+
+ );
+}
+
+export default DarkModeToggle;
diff --git a/src/components/admin/README_UNIFIED_STYLING.md b/src/components/admin/README_UNIFIED_STYLING.md
new file mode 100644
index 00000000..3e8375f1
--- /dev/null
+++ b/src/components/admin/README_UNIFIED_STYLING.md
@@ -0,0 +1,546 @@
+# 🎨 Unified Styling System - Admin Dashboard
+
+Sistem styling terpusat untuk admin dashboard Darmasaba dengan dukungan **dark mode**.
+
+**Berdasarkan spesifikasi:** `darkMode.md`
+
+---
+
+## 📋 Daftar Isi
+
+- [Konsep Utama](#konsep-utama)
+- [Dark Mode Palette](#dark-mode-palette)
+- [Struktur File](#struktur-file)
+- [Cara Menggunakan](#cara-menggunakan)
+- [Mengedit Style](#mengedit-style)
+- [Dark Mode Toggle](#dark-mode-toggle)
+- [Contoh Penggunaan](#contoh-penggunaan)
+
+---
+
+## 🎯 Konsep Utama
+
+**Satu File Edit = Semua Halaman Terupdate**
+
+Sebelumnya:
+- ❌ Style tersebar di 493 file `.tsx`
+- ❌ Hardcode warna di setiap komponen
+- ❌ Tidak ada konsistensi
+- ❌ Sulit maintain
+
+Sekarang:
+- ✅ Edit di **1 file** = semua halaman update
+- ✅ Component reusable
+- ✅ Konsisten di seluruh aplikasi
+- ✅ Dark mode otomatis sesuai spesifikasi `darkMode.md`
+
+---
+
+## 🌙 Dark Mode Palette
+
+### Background Layers (Dark Mode)
+| Layer | Token | Warna | Fungsi |
+|------|------|------|------|
+| Base | `bg.base` | `#0B1220` | Background utama aplikasi |
+| App | `bg.app` | `#0F172A` | Area sidebar |
+| Card | `bg.card` | `#162235` | Card / container |
+| Surface | `bg.surface` | `#1E2A3D` | Table header, tab, input |
+
+### Text Colors (Dark Mode)
+| Jenis | Token | Warna |
+|-----|------|------|
+| Primary | `text.primary` | `#E5E7EB` |
+| Secondary | `text.secondary` | `#9CA3AF` |
+| Muted | `text.muted` | `#6B7280` |
+
+### Accent & Actions (Dark Mode)
+| Fungsi | Warna |
+|------|------|
+| Primary Action | `#3B82F6` |
+| Hover | `#2563EB` |
+| Active | `#1D4ED8` |
+| Link | `#60A5FA` |
+
+### Borders (Dark Mode)
+| Token | Warna |
+|-----|------|
+| `border.default` | `#2A3A52` |
+| `border.soft` | `#22314A` |
+
+> **Catatan:** Light mode menggunakan palette original yang lebih terang
+
+---
+
+## 📁 Struktur File
+
+```
+src/
+├── utils/
+│ └── themeTokens.ts # 📦 PUSAT SEMUA STYLE (edit di sini!)
+├── state/
+│ └── darkModeStore.ts # 🌙 State management dark mode
+├── components/admin/
+│ ├── DarkModeToggle.tsx # 🌓 Toggle button
+│ ├── AdminThemeProvider.tsx # 🎨 Theme provider wrapper
+│ ├── UnifiedTypography.tsx # 📝 Text components (Title, Text)
+│ ├── UnifiedSurface.tsx # 📦 Card, Paper components
+│ └── README_UNIFIED_STYLING.md # 📖 Dokumentasi ini
+├── app/admin/
+│ ├── layout.tsx # ✅ Sudah diupdate dengan dark mode
+│ └── (dashboard)/
+│ └── _com/
+│ ├── header.tsx # ✅ Sudah diupdate
+│ ├── judulList.tsx # ✅ Sudah diupdate
+│ └── judulListTab.tsx # ✅ Sudah diupdate
+└── darkMode.md # 📐 Spesifikasi lengkap dark mode
+```
+
+---
+
+## 🚀 Cara Menggunakan
+
+### 1. **Untuk Developer: Edit Style Global**
+
+Edit file: `src/utils/themeTokens.ts`
+
+```typescript
+export const themeTokens = (isDark: boolean = false): ThemeTokens => {
+ const darkColors = {
+ bgBase: '#0B1220', // ← Edit warna dark mode di sini
+ bgCard: '#162235',
+ textPrimary: '#E5E7EB',
+ primaryAction: '#3B82F6',
+ // ... dan lainnya
+ };
+
+ return {
+ colors: {
+ primary: current.primaryAction,
+ bg: {
+ base: current.bgBase,
+ card: current.bgCard,
+ // ...
+ },
+ // ...
+ },
+ };
+};
+```
+
+### 2. **Menggunakan Components di Halaman**
+
+#### A. Typography Components
+
+```tsx
+import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography';
+
+// Heading - otomatis dark mode
+Judul Halaman
+Sub Judul
+Section Title
+Card Title
+
+// Text dengan color semantic
+Teks primary
+Teks secondary
+Teks muted
+Link text
+Brand color
+
+// Dengan weight
+Teks bold
+Teks medium
+```
+
+#### B. Surface Components
+
+```tsx
+import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface';
+
+// Card sederhana - border dan warna otomatis dark mode
+
+ Isi card
+
+
+// Card dengan sections
+
+
+ Header
+
+
+
+ Body content
+
+
+
+ Action
+
+
+
+// Divider dengan variant
+ {/* Default */}
+
+
+```
+
+#### C. Page Header Component
+
+```tsx
+import { UnifiedPageHeader } from '@/components/admin/UnifiedTypography';
+
+
+ Tambah Baru
+
+ }
+/>
+```
+
+### 3. **Menggunakan Theme Tokens Langsung**
+
+```tsx
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+
+function MyComponent() {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ return (
+
+
+ Konten dengan styling konsisten
+
+
+ );
+}
+```
+
+---
+
+## 🌓 Dark Mode Toggle
+
+### Otomatis di Header
+
+Dark mode toggle sudah terintegrasi di header admin dashboard. User bisa toggle dengan klik tombol 🌙/☀️.
+
+### Manual Toggle
+
+```tsx
+import { useDarkMode } from '@/state/darkModeStore';
+import { DarkModeToggle } from '@/components/admin/DarkModeToggle';
+
+function MyComponent() {
+ const { isDark, toggle } = useDarkMode();
+
+ return (
+
+
Current mode: {isDark ? 'Dark' : 'Light'}
+
+ {/* Gunakan component toggle */}
+
+
+ {/* Atau manual */}
+
Toggle
+
+ );
+}
+```
+
+### Persistensi
+
+Dark mode preference disimpan di `localStorage` dengan key `darmasaba-admin-dark-mode`.
+Preference akan tetap ada saat user refresh halaman atau kembali nanti.
+
+---
+
+## 📝 Contoh Penggunaan Lengkap
+
+### Contoh 1: List Page dengan Table
+
+```tsx
+'use client'
+import { UnifiedPageHeader, UnifiedText } from '@/components/admin/UnifiedTypography';
+import UnifiedCard from '@/components/admin/UnifiedSurface';
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { Button, Table, TableTr, TableTh, TableTd } from '@mantine/core';
+
+export default function DaftarBerita() {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ return (
+
+ {/* Header Halaman */}
+
+ + Tambah Berita
+
+ }
+ />
+
+ {/* Card untuk Table */}
+
+
+
+
+
+ Judul
+
+
+ Kategori
+
+
+
+
+ {data.map((item) => (
+
+
+ {item.judul}
+
+
+
+ {item.kategori}
+
+
+
+ ))}
+
+
+
+
+ );
+}
+```
+
+### Contoh 2: Detail Page
+
+```tsx
+import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography';
+import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface';
+
+export default function DetailBerita({ data }) {
+ return (
+
+
+ {data.judul}
+
+
+
+
+ Kategori
+ {data.kategori}
+
+
+
+
+
+ Deskripsi
+ {data.deskripsi}
+
+
+
+
+
+ Konten
+
+
+
+
+
+
+ Hapus
+ Edit
+
+
+
+ );
+}
+```
+
+---
+
+## 🎨 Mengedit Style
+
+### Edit Warna Dark Mode
+
+File: `src/utils/themeTokens.ts`
+
+```typescript
+const darkColors = {
+ // Background Layers
+ bgBase: '#0B1220', // ← Edit di sini
+ bgApp: '#0F172A',
+ bgCard: '#162235',
+ bgSurface: '#1E2A3D',
+
+ // Text
+ textPrimary: '#E5E7EB', // ← Edit di sini
+ textSecondary: '#9CA3AF',
+
+ // Accent
+ primaryAction: '#3B82F6', // ← Edit primary color
+};
+```
+
+### Edit Warna Light Mode
+
+```typescript
+const lightColors = {
+ bgBase: '#f6f9fc',
+ bgCard: '#ffffff',
+ textPrimary: '#1a1b1e',
+ primaryAction: baseColors['blue-button'], // Dari colors.ts
+};
+```
+
+### Edit Typography
+
+```typescript
+typography: {
+ h1: {
+ fz: '2rem', // ← Edit ukuran
+ fw: 700, // ← Edit weight
+ lh: 1.2, // ← Edit line height
+ },
+ body: {
+ fz: '1rem',
+ fw: 400,
+ lh: 1.5,
+ },
+}
+```
+
+### Edit Spacing & Radius
+
+```typescript
+spacing: {
+ xs: '0.625rem', // 10px
+ sm: '1rem', // 16px
+ md: '1.5rem', // 24px
+ lg: '2rem', // 32px
+}
+
+radius: {
+ sm: '0.5rem', // 8px
+ md: '0.75rem', // 12px
+ lg: '1rem', // 16px
+}
+```
+
+---
+
+## ✅ Checklist Migrasi
+
+Komponen yang sudah diupdate dengan dark mode:
+
+- ✅ `src/app/admin/layout.tsx`
+- ✅ `src/app/admin/(dashboard)/_com/header.tsx`
+- ✅ `src/app/admin/(dashboard)/_com/judulList.tsx`
+- ✅ `src/app/admin/(dashboard)/_com/judulListTab.tsx`
+- ✅ `src/components/admin/UnifiedTypography.tsx`
+- ✅ `src/components/admin/UnifiedSurface.tsx`
+- ✅ `src/components/admin/DarkModeToggle.tsx`
+- ✅ `src/utils/themeTokens.ts`
+
+Komponen yang perlu diupdate (TODO):
+
+- [ ] Komponen di `src/app/admin/(dashboard)/desa/`
+- [ ] Komponen di `src/app/admin/(dashboard)/ppid/`
+- [ ] Komponen di `src/app/admin/(dashboard)/kesehatan/`
+- [ ] Komponen di `src/app/admin/(dashboard)/pendidikan/`
+- [ ] Komponen di `src/app/admin/(dashboard)/ekonomi/`
+- [ ] Dan lain-lain...
+
+---
+
+## 📚 Referensi
+
+- [Dark Mode Specification](../../../darkMode.md) - Spesifikasi lengkap dark mode
+- [Mantine Theme System](https://mantine.dev/theming/theme-object/)
+- [Mantine Dark Mode](https://mantine.dev/theming/dark-mode/)
+- [Valtio State Management](https://github.com/pmndrs/valtio)
+
+---
+
+## 💡 Tips
+
+1. **Selalu gunakan unified components** untuk konsistensi dark/light mode
+2. **Edit di `themeTokens.ts`** untuk perubahan global
+3. **Test dark mode** setelah perubahan style
+4. **Gunakan color semantic** (`primary`, `secondary`, `muted`) bukan hex langsung
+5. **Jangan hardcode shadow** di dark mode (spec: "Jangan pakai shadow hitam")
+6. **Border harus terlihat** di dark mode (opacity > 20%)
+
+---
+
+## 🆘 Troubleshooting
+
+### Style tidak berubah setelah edit themeTokens.ts?
+
+1. Clear browser cache (Cmd+Shift+R / Ctrl+Shift+R)
+2. Restart dev server: `bun run dev`
+3. Pastikan komponen menggunakan unified components
+
+### Dark mode tidak berfungsi?
+
+1. Cek `darkModeStore.ts` sudah diimport
+2. Pastikan `useDarkMode()` hook digunakan
+3. Clear localStorage: `localStorage.clear()`
+4. Cek console untuk error
+
+### Border tidak terlihat di dark mode?
+
+Pastikan menggunakan `tokens.colors.border.default` atau `tokens.colors.border.soft`, bukan hardcode warna.
+
+### Component tidak re-render?
+
+1. Pastikan `'use client'` ada di file component
+2. Gunakan `useSnapshot()` jika menggunakan Valtio di non-event handler
+3. Cek console untuk error
+
+---
+
+## 📐 Spesifikasi Dark Mode
+
+Untuk spesifikasi lengkap dark mode (layout rules, table styles, button rules, dll), lihat:
+**[`darkMode.md`](../../../darkMode.md)**
+
+Highlights:
+- ✅ Background layers berbeda (base, app, card, surface)
+- ✅ Border wajib terlihat (tidak flat)
+- ✅ Active state dengan accent bar (2-3px)
+- ✅ Tidak pakai shadow hitam
+- ✅ Hover state dengan background soft
+- ✅ Text kontras terbaca
+
+---
+
+**Last Updated:** February 20, 2026
+**Version:** 2.0.0 (Dark Mode Ready)
+**Based on:** darkMode.md specification
diff --git a/src/components/admin/UnifiedSurface.tsx b/src/components/admin/UnifiedSurface.tsx
new file mode 100644
index 00000000..c00f911b
--- /dev/null
+++ b/src/components/admin/UnifiedSurface.tsx
@@ -0,0 +1,252 @@
+'use client';
+
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens } from '@/utils/themeTokens';
+import { Paper, Box, BoxProps, Divider, DividerProps } from '@mantine/core';
+import React from 'react';
+
+/**
+ * Unified Surface Components
+ *
+ * Komponen container/card dengan styling konsisten
+ * Mendukung dark mode sesuai spesifikasi darkMode.md
+ *
+ * Usage:
+ * import { UnifiedCard, UnifiedDivider } from '@/components/admin/UnifiedSurface';
+ *
+ *
+ * Title
+ * Content
+ *
+ */
+
+// ============================================================================
+// Unified Card Component
+ * ============================================================================
+
+interface UnifiedCardProps extends BoxProps {
+ withBorder?: boolean;
+ shadow?: 'none' | 'sm' | 'md' | 'lg';
+ padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+ hoverable?: boolean;
+ children: React.ReactNode;
+}
+
+export function UnifiedCard({
+ withBorder = true,
+ shadow = 'none', // Sesuai spec: Jangan pakai shadow hitam
+ padding = 'md',
+ hoverable = false,
+ children,
+ style,
+ ...props
+}: UnifiedCardProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const getPadding = () => {
+ switch (padding) {
+ case 'none':
+ return 0;
+ case 'xs':
+ return tokens.spacing.xs;
+ case 'sm':
+ return tokens.spacing.sm;
+ case 'md':
+ return tokens.spacing.md;
+ case 'lg':
+ return tokens.spacing.lg;
+ case 'xl':
+ return tokens.spacing.xl;
+ default:
+ return tokens.spacing.md;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+// ============================================================================
+// Unified Card Section Components
+// ============================================================================
+
+interface UnifiedCardSectionProps {
+ children: React.ReactNode;
+ padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg';
+ border?: 'none' | 'top' | 'bottom';
+ style?: React.CSSProperties;
+}
+
+UnifiedCard.Header = function UnifiedCardHeader({
+ children,
+ padding = 'md',
+ border = 'bottom',
+ style,
+}: UnifiedCardSectionProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const getPadding = () => {
+ switch (padding) {
+ case 'none':
+ return 0;
+ case 'xs':
+ return tokens.spacing.xs;
+ case 'sm':
+ return tokens.spacing.sm;
+ case 'md':
+ return tokens.spacing.md;
+ case 'lg':
+ return tokens.spacing.lg;
+ default:
+ return tokens.spacing.md;
+ }
+ };
+
+ const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none';
+ const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none';
+
+ return (
+
+ {children}
+
+ );
+};
+
+UnifiedCard.Body = function UnifiedCardBody({
+ children,
+ padding = 'md',
+ style,
+}: UnifiedCardSectionProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const getPadding = () => {
+ switch (padding) {
+ case 'none':
+ return 0;
+ case 'xs':
+ return tokens.spacing.xs;
+ case 'sm':
+ return tokens.spacing.sm;
+ case 'md':
+ return tokens.spacing.md;
+ case 'lg':
+ return tokens.spacing.lg;
+ default:
+ return tokens.spacing.md;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+UnifiedCard.Footer = function UnifiedCardFooter({
+ children,
+ padding = 'md',
+ border = 'top',
+ style,
+}: UnifiedCardSectionProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const getPadding = () => {
+ switch (padding) {
+ case 'none':
+ return 0;
+ case 'xs':
+ return tokens.spacing.xs;
+ case 'sm':
+ return tokens.spacing.sm;
+ case 'md':
+ return tokens.spacing.md;
+ case 'lg':
+ return tokens.spacing.lg;
+ default:
+ return tokens.spacing.md;
+ }
+ };
+
+ const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none';
+ const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none';
+
+ return (
+
+ {children}
+
+ );
+};
+
+// ============================================================================
+// Unified Divider Component
+// ============================================================================
+
+interface UnifiedDividerProps extends DividerProps {
+ variant?: 'default' | 'soft' | 'strong';
+}
+
+export function UnifiedDivider({
+ variant = 'soft', // Default soft sesuai spec
+ my = 'md',
+ ...props
+}: UnifiedDividerProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const getColor = () => {
+ switch (variant) {
+ case 'default':
+ return tokens.colors.border.default;
+ case 'soft':
+ return tokens.colors.border.soft;
+ case 'strong':
+ return tokens.colors.border.strong;
+ default:
+ return tokens.colors.border.soft;
+ }
+ };
+
+ return ;
+}
+
+export default UnifiedCard;
diff --git a/src/components/admin/UnifiedTypography.tsx b/src/components/admin/UnifiedTypography.tsx
new file mode 100644
index 00000000..565be609
--- /dev/null
+++ b/src/components/admin/UnifiedTypography.tsx
@@ -0,0 +1,268 @@
+'use client';
+
+import { useDarkMode } from '@/state/darkModeStore';
+import { themeTokens, getResponsiveFz } from '@/utils/themeTokens';
+import { Text, Title, Box, BoxProps } from '@mantine/core';
+import React from 'react';
+
+/**
+ * Unified Typography Components
+ *
+ * Komponen text dengan styling konsisten di seluruh aplikasi
+ * Mendukung dark mode sesuai spesifikasi darkMode.md
+ *
+ * Usage:
+ * import { UnifiedText, UnifiedTitle } from '@/components/admin/UnifiedTypography';
+ *
+ * Judul Halaman
+ * Konten teks
+ */
+
+// ============================================================================
+// Unified Title Component
+// ============================================================================
+
+interface UnifiedTitleProps {
+ order?: 1 | 2 | 3 | 4 | 5 | 6;
+ children: React.ReactNode;
+ align?: 'left' | 'center' | 'right';
+ color?: 'primary' | 'secondary' | 'brand' | string;
+ mb?: string;
+ mt?: string;
+ ml?: string;
+ mr?: string;
+ mx?: string;
+ my?: string;
+ style?: React.CSSProperties;
+}
+
+export function UnifiedTitle({
+ order = 1,
+ children,
+ align = 'left',
+ color = 'primary',
+ mb,
+ mt,
+ ml,
+ mr,
+ mx,
+ my,
+ style,
+}: UnifiedTitleProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+ const responsiveFz = getResponsiveFz(isDark);
+
+ const getTypography = () => {
+ switch (order) {
+ case 1:
+ return tokens.typography.h1;
+ case 2:
+ return tokens.typography.h2;
+ case 3:
+ return tokens.typography.h3;
+ case 4:
+ return tokens.typography.h4;
+ default:
+ return tokens.typography.body;
+ }
+ };
+
+ const typo = getTypography();
+
+ const getColor = () => {
+ if (color === 'primary') return tokens.colors.text.primary;
+ if (color === 'secondary') return tokens.colors.text.secondary;
+ if (color === 'brand') return tokens.colors.brand;
+ return color;
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+// ============================================================================
+// Unified Text Component
+// ============================================================================
+
+interface UnifiedTextProps {
+ size?: 'small' | 'body' | 'label';
+ weight?: 'normal' | 'medium' | 'bold';
+ children: React.ReactNode;
+ align?: 'left' | 'center' | 'right';
+ color?: 'primary' | 'secondary' | 'tertiary' | 'muted' | 'brand' | 'link' | string;
+ lineClamp?: number;
+ truncate?: 'start' | 'end' | 'middle' | boolean;
+ span?: boolean;
+ style?: React.CSSProperties;
+}
+
+export function UnifiedText({
+ size = 'body',
+ weight = 'normal',
+ children,
+ align = 'left',
+ color = 'primary',
+ lineClamp,
+ truncate,
+ span = false,
+ style,
+}: UnifiedTextProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ const getTypography = () => {
+ switch (size) {
+ case 'small':
+ return tokens.typography.small;
+ case 'label':
+ return tokens.typography.label;
+ default:
+ return tokens.typography.body;
+ }
+ };
+
+ const getWeight = () => {
+ switch (weight) {
+ case 'normal':
+ return 400;
+ case 'medium':
+ return 500;
+ case 'bold':
+ return 700;
+ default:
+ return 400;
+ }
+ };
+
+ const getColor = () => {
+ switch (color) {
+ case 'primary':
+ return tokens.colors.text.primary;
+ case 'secondary':
+ return tokens.colors.text.secondary;
+ case 'tertiary':
+ return tokens.colors.text.tertiary;
+ case 'muted':
+ return tokens.colors.text.muted;
+ case 'brand':
+ return tokens.colors.brand;
+ case 'link':
+ return tokens.colors.text.link;
+ default:
+ return color;
+ }
+ };
+
+ const typo = getTypography();
+ const fw = getWeight();
+ const textColor = getColor();
+
+ if (span) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+// ============================================================================
+// Unified Page Header Component
+//
+// Header standar untuk setiap halaman admin
+// Sesuai spesifikasi: Section Header dengan font weight lebih besar
+// ============================================================================
+
+interface UnifiedPageHeaderProps extends BoxProps {
+ title: string;
+ subtitle?: string;
+ action?: React.ReactNode;
+ showBorder?: boolean;
+}
+
+export function UnifiedPageHeader({
+ title,
+ subtitle,
+ action,
+ showBorder = true,
+ style,
+ ...props
+}: UnifiedPageHeaderProps) {
+ const { isDark } = useDarkMode();
+ const tokens = themeTokens(isDark);
+
+ return (
+
+
+
+ {title}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {action &&
{action}
}
+
+
+ );
+}
+
+export default UnifiedText;
diff --git a/src/lib/api-auth.ts b/src/lib/api-auth.ts
new file mode 100644
index 00000000..d639b296
--- /dev/null
+++ b/src/lib/api-auth.ts
@@ -0,0 +1,84 @@
+/**
+ * Authentication helper untuk API endpoints
+ *
+ * Usage:
+ * import { requireAuth } from "@/lib/api-auth";
+ *
+ * export default async function myEndpoint(context: Context) {
+ * const authResult = await requireAuth(context);
+ * if (!authResult.authenticated) {
+ * return authResult.response;
+ * }
+ * // Lanjut proses dengan authResult.user
+ * }
+ */
+
+import { getSession } from "@/lib/session";
+
+export type AuthResult =
+ | { authenticated: true; user: any }
+ | { authenticated: false; response: Response };
+
+export async function requireAuth(context: any): Promise {
+ try {
+ // Cek session dari cookies
+ const session = await getSession();
+
+ if (!session || !session.user) {
+ return {
+ authenticated: false,
+ response: new Response(JSON.stringify({
+ success: false,
+ message: "Unauthorized - Silakan login terlebih dahulu"
+ }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' }
+ })
+ };
+ }
+
+ // Check jika user masih aktif
+ if (!session.user.isActive) {
+ return {
+ authenticated: false,
+ response: new Response(JSON.stringify({
+ success: false,
+ message: "Akun Anda tidak aktif. Hubungi administrator."
+ }), {
+ status: 403,
+ headers: { 'Content-Type': 'application/json' }
+ })
+ };
+ }
+
+ return {
+ authenticated: true,
+ user: session.user
+ };
+ } catch (error) {
+ console.error("Auth error:", error);
+ return {
+ authenticated: false,
+ response: new Response(JSON.stringify({
+ success: false,
+ message: "Authentication error"
+ }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' }
+ })
+ };
+ }
+}
+
+/**
+ * Optional auth - tidak error jika tidak authenticated
+ * Berguna untuk endpoint yang bisa diakses public atau private
+ */
+export async function optionalAuth(context: any): Promise {
+ try {
+ const session = await getSession();
+ return session?.user || null;
+ } catch (error) {
+ return null;
+ }
+}
diff --git a/src/lib/session.ts b/src/lib/session.ts
new file mode 100644
index 00000000..10c4fc57
--- /dev/null
+++ b/src/lib/session.ts
@@ -0,0 +1,68 @@
+/**
+ * Session helper menggunakan iron-session
+ *
+ * Usage:
+ * import { getSession } from "@/lib/session";
+ *
+ * const session = await getSession();
+ * if (session?.user) {
+ * // User authenticated
+ * }
+ */
+
+import { getIronSession } from 'iron-session';
+import { cookies } from 'next/headers';
+
+export type SessionData = {
+ user?: {
+ id: string;
+ name: string;
+ roleId: number;
+ menuIds?: string[] | null;
+ isActive?: boolean;
+ };
+};
+
+export type Session = SessionData & {
+ save: () => Promise;
+ destroy: () => Promise;
+};
+
+const SESSION_OPTIONS = {
+ cookieName: 'desa-session',
+ password: process.env.SESSION_PASSWORD || 'default-password-change-in-production',
+ cookieOptions: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ sameSite: 'lax' as const,
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ },
+};
+
+export async function getSession(): Promise {
+ try {
+ const cookieStore = await cookies();
+ const session = await getIronSession(
+ cookieStore,
+ SESSION_OPTIONS
+ );
+
+ return session;
+ } catch (error) {
+ console.error('Session error:', error);
+ return null;
+ }
+}
+
+export async function destroySession(): Promise {
+ try {
+ const cookieStore = await cookies();
+ const session = await getIronSession(
+ cookieStore,
+ SESSION_OPTIONS
+ );
+ await session.destroy();
+ } catch (error) {
+ console.error('Destroy session error:', error);
+ }
+}
diff --git a/src/state/darkModeStore.ts b/src/state/darkModeStore.ts
new file mode 100644
index 00000000..cf37d86d
--- /dev/null
+++ b/src/state/darkModeStore.ts
@@ -0,0 +1,77 @@
+/**
+ * Dark Mode State Management
+ *
+ * Menggunakan Valtio untuk global state
+ * Persist ke localStorage
+ *
+ * Usage:
+ * import { darkModeStore } from '@/state/darkModeStore';
+ *
+ * // Toggle
+ * darkModeStore.toggle();
+ *
+ * // Set explicitly
+ * darkModeStore.setDarkMode(true);
+ *
+ * // Get current state
+ * const isDark = darkModeStore.isDark;
+ */
+
+import { proxy, useSnapshot } from 'valtio';
+
+const STORAGE_KEY = 'darmasaba-admin-dark-mode';
+
+// Initialize from localStorage or default to light mode
+const getInitialDarkMode = (): boolean => {
+ if (typeof window === 'undefined') return false;
+
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored !== null) {
+ return stored === 'true';
+ }
+
+ // Default to light mode for first-time users
+ // System preference is NOT used as default to ensure consistent UX
+ return false;
+};
+
+class DarkModeStore {
+ public isDark: boolean;
+
+ constructor() {
+ this.isDark = getInitialDarkMode();
+ }
+
+ public toggle() {
+ this.isDark = !this.isDark;
+ this.persist();
+ }
+
+ public setDarkMode(value: boolean) {
+ this.isDark = value;
+ this.persist();
+ }
+
+ private persist() {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem(STORAGE_KEY, String(this.isDark));
+ }
+ }
+}
+
+// Create proxy instance
+const store = new DarkModeStore();
+
+export const darkModeStore = proxy(store);
+
+// Hook untuk menggunakan dark mode state di React components
+export const useDarkMode = () => {
+ const snapshot = useSnapshot(darkModeStore);
+ return {
+ isDark: snapshot.isDark,
+ toggle: () => darkModeStore.toggle(),
+ setDarkMode: (value: boolean) => darkModeStore.setDarkMode(value),
+ };
+};
+
+export default darkModeStore;
diff --git a/src/styles/dark-mode-table.css b/src/styles/dark-mode-table.css
new file mode 100644
index 00000000..ecc08032
--- /dev/null
+++ b/src/styles/dark-mode-table.css
@@ -0,0 +1,31 @@
+/**
+ * Dark Mode Table Styles
+ *
+ * Override Mantine table hover styles untuk dark mode
+ * Agar teks putih tetap terlihat saat hover
+ */
+
+/* Dark mode table hover */
+[data-mantine-color-scheme="dark"] {
+ /* Table hover */
+ .mantine-Table-tr:hover {
+ background-color: rgba(255, 255, 255, 0.08) !important;
+ }
+
+ /* Table striped hover */
+ .mantine-Table-striped .mantine-Table-tr:nth-of-type(odd):hover {
+ background-color: rgba(255, 255, 255, 0.08) !important;
+ }
+
+ /* Table with column borders */
+ .mantine-Table-withColumnBorders .mantine-Table-tr:hover {
+ background-color: rgba(255, 255, 255, 0.08) !important;
+ }
+}
+
+/* Light mode table hover - default Mantine behavior */
+[data-mantine-color-scheme="light"] {
+ .mantine-Table-tr:hover {
+ background-color: rgba(0, 0, 0, 0.02) !important;
+ }
+}
diff --git a/src/utils/themeTokens.ts b/src/utils/themeTokens.ts
new file mode 100644
index 00000000..75c133a0
--- /dev/null
+++ b/src/utils/themeTokens.ts
@@ -0,0 +1,383 @@
+/**
+ * Unified Theme Tokens for Admin Dashboard
+ *
+ * Berdasarkan spesifikasi: darkMode.md
+ *
+ * Semua styling constants disimpan di sini untuk konsistensi
+ * Edit di sini = edit di seluruh aplikasi
+ *
+ * Usage:
+ * import { themeTokens } from '@/utils/themeTokens';
+ *
+ * // Light mode (default)
+ * const tokens = themeTokens(false);
+ *
+ * // Dark mode
+ * const tokens = themeTokens(true);
+ */
+
+export type ThemeTokens = {
+ // Colors
+ colors: {
+ primary: string;
+ primaryLight: string;
+ primaryDark: string;
+ gradient: {
+ from: string;
+ to: string;
+ };
+ // Backgrounds
+ bg: {
+ base: string;
+ main: string;
+ app: string;
+ surface: string;
+ surfaceElevated: string;
+ header: string;
+ navbar: string;
+ card: string;
+ hover: string;
+ tableHeader: string;
+ tableHover: string;
+ };
+ // Text
+ text: {
+ primary: string;
+ secondary: string;
+ tertiary: string;
+ muted: string;
+ brand: string;
+ inverse: string;
+ link: string;
+ };
+ // Borders
+ border: {
+ default: string;
+ soft: string;
+ strong: string;
+ };
+ // Status
+ success: string;
+ warning: string;
+ error: string;
+ info: string;
+ };
+
+ // Typography
+ typography: {
+ h1: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ h2: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ h3: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ h4: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ body: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ small: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ label: {
+ fz: string;
+ fw: number;
+ lh: number;
+ };
+ };
+
+ // Spacing
+ spacing: {
+ xs: string;
+ sm: string;
+ md: string;
+ lg: string;
+ xl: string;
+ };
+
+ // Border Radius
+ radius: {
+ sm: string;
+ md: string;
+ lg: string;
+ xl: string;
+ };
+
+ // Shadows
+ shadows: {
+ none: string;
+ sm: string;
+ md: string;
+ lg: string;
+ };
+
+ // Layout
+ layout: {
+ headerHeight: number;
+ navbarWidth: {
+ base: number;
+ sm: number;
+ lg: number;
+ };
+ };
+};
+
+export const themeTokens = (isDark: boolean = false): ThemeTokens => {
+ // Base colors - tetap menggunakan colors.ts sebagai base untuk light mode
+ const baseColors = {
+ 'orange': '#FCAE00',
+ 'blue-button': '#0A4E78',
+ 'blue-button-1': '#E5F2FA',
+ 'blue-button-2': '#B8DAEF',
+ 'blue-button-3': '#8AC1E3',
+ 'blue-button-4': '#5DA9D8',
+ 'blue-button-5': '#2F91CC',
+ 'blue-button-6': '#083F61',
+ 'blue-button-7': '#062F49',
+ 'blue-button-8': '#041F32',
+ 'blue-button-trans': '#628EC6',
+ 'white-1': '#FBFBFC',
+ 'white-trans-1': 'rgba(255, 255, 255, 0.5)',
+ 'white-trans-2': 'rgba(255, 255, 255, 0.7)',
+ 'white-trans-3': 'rgba(255, 255, 255, 0.9)',
+ 'grey-1': '#F4F5F6',
+ 'grey-2': '#CBCACD',
+ 'Bg': '#D1d9e8',
+ 'BG-trans': '#B1C5F2',
+ };
+
+ /**
+ * DARK MODE PALETTE
+ * Berdasarkan spesifikasi: darkMode.md
+ */
+ const darkColors = {
+ // Background Layers
+ bgBase: '#0B1220',
+ bgApp: '#0F172A',
+ bgCard: '#162235',
+ bgSurface: '#1E2A3D',
+
+ // Borders
+ borderDefault: '#2A3A52',
+ borderSoft: '#22314A',
+
+ // Text
+ textPrimary: '#E5E7EB',
+ textSecondary: '#9CA3AF',
+ textMuted: '#6B7280',
+ textInverse: '#020617',
+
+ // Accent & Actions
+ primaryAction: '#3B82F6',
+ primaryHover: '#2563EB',
+ primaryActive: '#1D4ED8',
+ link: '#60A5FA',
+
+ // Status
+ success: '#22C55E',
+ warning: '#FACC15',
+ error: '#EF4444',
+ info: '#38BDF8',
+
+ // Hover states
+ hoverSoft: 'rgba(255,255,255,0.03)',
+ hoverMedium: 'rgba(255,255,255,0.04)',
+ activeAccent: 'rgba(59,130,246,0.15)',
+ };
+
+ /**
+ * LIGHT MODE PALETTE
+ * Original light theme
+ */
+ const lightColors = {
+ bgBase: '#f6f9fc',
+ bgApp: '#ffffff',
+ bgCard: '#ffffff',
+ bgSurface: '#f8fafc',
+ borderDefault: '#e2e8f0',
+ borderSoft: '#e9ecef',
+ textPrimary: '#1a1b1e',
+ textSecondary: '#495057',
+ textMuted: '#868e96',
+ textInverse: '#ffffff',
+ primaryAction: baseColors['blue-button'],
+ primaryHover: '#083F61',
+ primaryActive: '#062F49',
+ link: '#2563eb',
+ hoverSoft: 'rgba(25, 113, 194, 0.03)',
+ hoverMedium: 'rgba(25, 113, 194, 0.05)',
+ activeAccent: 'rgba(25, 113, 194, 0.1)',
+ };
+
+ const current = isDark ? darkColors : lightColors;
+
+ return {
+ colors: {
+ primary: current.primaryAction,
+ primaryLight: isDark ? current.activeAccent : baseColors['blue-button-1'],
+ primaryDark: current.primaryActive,
+ gradient: {
+ from: current.primaryAction,
+ to: isDark ? '#60A5FA' : '#228be6',
+ },
+ bg: {
+ base: current.bgBase,
+ main: isDark ? current.bgBase : 'linear-gradient(180deg, #fdfdfd, #f6f9fc)',
+ app: current.bgApp,
+ surface: current.bgSurface,
+ surfaceElevated: isDark ? '#253347' : '#ffffff',
+ header: isDark
+ ? `linear-gradient(180deg, ${current.bgApp} 0%, ${current.bgBase} 100%)`
+ : 'linear-gradient(90deg, #ffffff, #f9fbff)',
+ navbar: current.bgApp,
+ card: current.bgCard,
+ hover: current.hoverMedium,
+ tableHeader: current.bgSurface,
+ tableHover: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)',
+ },
+ text: {
+ primary: current.textPrimary,
+ secondary: current.textSecondary,
+ tertiary: current.textMuted,
+ muted: current.textMuted,
+ brand: current.primaryAction,
+ inverse: current.textInverse,
+ link: current.link,
+ },
+ border: {
+ default: current.borderDefault,
+ soft: current.borderSoft,
+ strong: isDark ? '#3A4A62' : '#ced4da',
+ },
+ success: current.success,
+ warning: current.warning,
+ error: current.error,
+ info: current.info,
+ },
+
+ typography: {
+ h1: {
+ fz: isDark ? '2rem' : '2.25rem',
+ fw: 700,
+ lh: 1.2,
+ },
+ h2: {
+ fz: isDark ? '1.75rem' : '2rem',
+ fw: 700,
+ lh: 1.25,
+ },
+ h3: {
+ fz: isDark ? '1.5rem' : '1.75rem',
+ fw: 700,
+ lh: 1.3,
+ },
+ h4: {
+ fz: isDark ? '1.25rem' : '1.5rem',
+ fw: 600,
+ lh: 1.35,
+ },
+ body: {
+ fz: '1rem',
+ fw: 400,
+ lh: 1.5,
+ },
+ small: {
+ fz: '0.875rem',
+ fw: 400,
+ lh: 1.4,
+ },
+ label: {
+ fz: '0.75rem',
+ fw: 600,
+ lh: 1.4,
+ },
+ },
+
+ spacing: {
+ xs: '0.625rem',
+ sm: '1rem',
+ md: '1.5rem',
+ lg: '2rem',
+ xl: '2.5rem',
+ },
+
+ radius: {
+ sm: '0.5rem', // 8px
+ md: '0.75rem', // 12px
+ lg: '1rem', // 16px
+ xl: '1.25rem', // 20px
+ },
+
+ shadows: {
+ none: 'none',
+ sm: isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)',
+ md: isDark ? '0 4px 6px rgba(0,0,0,0.3)' : '0 4px 6px rgba(0,0,0,0.1)',
+ lg: isDark ? '0 10px 15px rgba(0,0,0,0.3)' : '0 10px 15px rgba(0,0,0,0.1)',
+ },
+
+ layout: {
+ headerHeight: 64,
+ navbarWidth: {
+ base: 260,
+ sm: 280,
+ lg: 300,
+ },
+ },
+ };
+};
+
+// Export default theme instances
+export const lightTheme = themeTokens(false);
+export const darkTheme = themeTokens(true);
+
+// Helper untuk mendapatkan responsive font size
+export const getResponsiveFz = (isDark: boolean = false) => ({
+ base: isDark ? 'md' : 'lg',
+ md: isDark ? 'lg' : 'xl',
+});
+
+// Helper untuk mendapatkan color berdasarkan state
+export const getActiveColor = (isActive: boolean, isDark: boolean = false) =>
+ isActive ? themeTokens(isDark).colors.primary : isDark ? themeTokens(isDark).colors.text.secondary : 'gray';
+
+// Helper untuk mendapatkan background hover
+export const getHoverBackground = (isActive: boolean, isDark: boolean = false) => {
+ const tokens = themeTokens(isDark);
+ return isActive
+ ? tokens.colors.bg.hover
+ : tokens.colors.bg.hover;
+};
+
+// Helper untuk active state dengan accent bar (sidebar)
+export const getActiveStateStyles = (isActive: boolean, isDark: boolean = false) => {
+ const tokens = themeTokens(isDark);
+
+ if (isActive) {
+ return {
+ backgroundColor: isDark ? tokens.colors.bg.hover : 'rgba(25, 113, 194, 0.1)',
+ borderLeft: isDark ? `3px solid ${tokens.colors.primary}` : '3px solid #1971c2',
+ };
+ }
+
+ return {
+ '&:hover': {
+ backgroundColor: tokens.colors.bg.hover,
+ },
+ };
+};
diff --git a/test-berita-state.ts b/test-berita-state.ts
new file mode 100644
index 00000000..cea6b5f5
--- /dev/null
+++ b/test-berita-state.ts
@@ -0,0 +1,28 @@
+import stateDashboardBerita from './src/app/admin/(dashboard)/_state/desa/berita';
+
+// Simple validation to check if the state structure is correct
+console.log('Testing Berita State Structure...');
+
+// Check if main objects exist
+console.log('✓ Berita object exists:', !!stateDashboardBerita.berita);
+console.log('✓ KategoriBerita object exists:', !!stateDashboardBerita.kategoriBerita);
+
+// Check if required methods exist in berita
+const berita = stateDashboardBerita.berita;
+console.log('✓ Berita.create exists:', !!berita.create);
+console.log('✓ Berita.findMany exists:', !!berita.findMany);
+console.log('✓ Berita.findUnique exists:', !!berita.findUnique);
+console.log('✓ Berita.delete exists:', !!berita.delete);
+console.log('✓ Berita.edit exists:', !!berita.edit);
+console.log('✓ Berita.findFirst exists:', !!berita.findFirst);
+console.log('✓ Berita.findRecent exists:', !!berita.findRecent);
+
+// Check if required methods exist in kategoriBerita
+const kategoriBerita = stateDashboardBerita.kategoriBerita;
+console.log('✓ KategoriBerita.create exists:', !!kategoriBerita.create);
+console.log('✓ KategoriBerita.findMany exists:', !!kategoriBerita.findMany);
+console.log('✓ KategoriBerita.findUnique exists:', !!kategoriBerita.findUnique);
+console.log('✓ KategoriBerita.delete exists:', !!kategoriBerita.delete);
+console.log('✓ KategoriBerita.update exists:', !!kategoriBerita.update);
+
+console.log('\\nAll state properties are correctly defined!');
\ No newline at end of file
diff --git a/test-results/.last-run.json b/test-results/.last-run.json
new file mode 100644
index 00000000..f950cf91
--- /dev/null
+++ b/test-results/.last-run.json
@@ -0,0 +1,6 @@
+{
+ "status": "failed",
+ "failedTests": [
+ "84e7eb052e8cbae1c9f0-6067b767debcd3745af5"
+ ]
+}
\ No newline at end of file
diff --git a/test-results/homepage-homepage-has-correct-title-and-content-chromium/error-context.md b/test-results/homepage-homepage-has-correct-title-and-content-chromium/error-context.md
new file mode 100644
index 00000000..c0cf174c
--- /dev/null
+++ b/test-results/homepage-homepage-has-correct-title-and-content-chromium/error-context.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/types/env.d.ts b/types/env.d.ts
index 84193e50..00da8973 100644
--- a/types/env.d.ts
+++ b/types/env.d.ts
@@ -1,15 +1,38 @@
declare namespace NodeJS {
interface ProcessEnv {
+ // Database
DATABASE_URL?: string;
+
+ // Seafile Configuration
SEAFILE_TOKEN?: string;
SEAFILE_REPO_ID?: string;
+ SEAFILE_BASE_URL?: string;
+ SEAFILE_PUBLIC_SHARE_TOKEN?: string;
+ SEAFILE_URL?: string;
+
+ // Upload/Download Directories
WIBU_UPLOAD_DIR?: string;
WIBU_DOWNLOAD_DIR?: string;
+
+ // Email Configuration
EMAIL_USER?: string;
EMAIL_PASS?: string;
+
+ // Application URLs
NEXT_PUBLIC_BASE_URL?: string;
+
+ // Authentication
NEXTAUTH_SECRET?: string;
NEXTAUTH_URL?: string;
- SEAFILE_URL?: string;
+ BASE_SESSION_KEY?: string;
+ BASE_TOKEN_KEY?: string;
+ NEXT_PUBLIC_BASE_SESSION_KEY?: string;
+ NEXT_PUBLIC_BASE_TOKEN_KEY?: string;
+
+ // API Keys
+ ELEVENLABS_API_KEY?: string;
+
+ // Environment
+ NODE_ENV?: 'development' | 'production' | 'test';
}
}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 00000000..1cb57762
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vitest/config';
+import path from 'path';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './__tests__/setup.ts',
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+});