Compare commits

...

19 Commits

Author SHA1 Message Date
b1d28a8322 fix-admin-menu-desa-profile 2026-02-25 15:25:51 +08:00
b86a3a85c3 fix: force default light mode for public pages and admin
- Set defaultColorScheme='light' in root MantineProvider
- Change darkModeStore default from system preference to false (light)
- Add MantineProvider with light theme to darmasaba/layout.tsx
- Remove dark mode dependency from ModuleView component
- Prevent system color scheme from affecting initial page load

This ensures consistent light mode on first visit for both
public pages and admin panel, regardless of OS settings.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 10:45:27 +08:00
fd63bb0fd4 feat: implement dark mode support & fix Prisma schema validation
- Add dark mode toggle component in admin header
- Integrate dark mode store across admin layout and components
- Add unified typography and surface components for consistent theming
- Implement smooth transitions for dark/light mode switching
- Fix Prisma schema: remove @default(null) from DateTime? fields
- Update form validation for inovasi, lingkungan, and pendidikan modules
- Add form validation and improve UX across multiple admin pages

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 10:41:48 +08:00
f2c9a922a6 fix(profil-module): QC improvements based on QC-PROFIL-MODULE.md
- Fix fetch method inconsistency (convert to ApiFetch)
  - programInovasi: findUnique, delete, update methods
  - mediaSosial: findUnique, delete, update methods
- Add loading state to findUnique operations
- Fix iconUrl validation (make optional instead of required)
- Add DOMPurify for HTML sanitization (XSS protection)
  - program-inovasi page.tsx (list & detail)
- Remove console.log in production (use dev-only logging)
- Install dompurify and @types/dompurify

Security: Prevent XSS attacks by sanitizing HTML content
Consistency: Use ApiFetch for all API operations
UX: Proper loading states for better user feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 15:11:00 +08:00
92b24440fe fix: Quality Control improvements & bug fixes
- APBDes: Fix edit form original data tracking (imageId, fileId)
- APBDes: Update formula consistency in state
- PPID modules: Various UI improvements and bug fixes
- PPID Profil: Preview and edit page improvements
- PPID Dasar Hukum: Page structure improvements
- PPID Visi Misi: Page structure improvements
- PPID Struktur: Posisi organisasi page improvements
- PPID Daftar Informasi: Edit page improvements
- Auth login: Route improvements
- Update dependencies (package.json, bun.lockb)
- Update seed data
- Update .gitignore

QC Reports added:
- QC-APBDES-MODULE.md
- QC-PROFIL-MODULE.md
- QC-SDGS-DESA.md
- QC-DESA-ANTI-KORUPSI.md
- QC-PRESTASI-DESA-MODULE.md
- QC-PPID-PROFIL-MODULE.md
- QC-STRUKTUR-PPID-MODULE.md
- QC-VISI-MISI-PPID-MODULE.md
- QC-DASAR-HUKUM-PPID-MODULE.md
- QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md
- QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md
- QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md
- QC-IKM-MODULE.md

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 14:38:28 +08:00
f0558aa0d0 feat: implement dark mode support for admin layout and components
- Add dark mode toggle component in admin header
- Integrate dark mode store across admin layout and child components
- Update header, judulList, and judulListTab components with theme tokens
- Add unified typography components for consistent theming
- Implement smooth transitions for dark/light mode switching
- Add mounted state to prevent hydration mismatches
- Style navbar with dark mode aware colors and hover states
- Update button styles with gradient effects for both themes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 10:48:00 +08:00
8132609ccb feat: add form validation for inovasi, lingkungan, and pendidikan modules
- Added isFormValid() and isHtmlEmpty() helper functions for form validation
- Disabled submit buttons when required fields are empty across multiple admin and public pages
- Applied consistent validation pattern for creating and editing records
- Commented out WhatsApp OTP sending in login route for debugging/testing
- Fixed path in NavbarMainMenu tooltip action
2026-02-20 15:08:41 +08:00
1ddc1d7eac feat: add form validation for ekonomi module admin pages
- Added isFormValid() and isHtmlEmpty() helper functions
- Disabled submit buttons when required fields are empty
- Applied consistent validation pattern across all create/edit pages
- Validated fields: nama, deskripsi, tahun, jumlah, value, icon, statistik, and more
- Edit pages allow existing data, create pages require all fields

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-18 16:13:20 +08:00
aa354992e7 feat: add form validation for keamanan module admin pages
- Added isFormValid() and isHtmlEmpty() helper functions
- Disabled submit buttons when required fields are empty
- Applied consistent validation pattern across all create/edit pages
- Validated fields: judul, deskripsi, lokasi, tanggal, status, kronologi, penanganan, link video, and image uploads
- Edit pages allow existing images, create pages require new uploads

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-18 11:22:30 +08:00
d43b07c2ef feat: add form validation for kesehatan module admin pages
- Added isFormValid() and isHtmlEmpty() helper functions
- Disabled submit buttons when required fields are empty
- Applied consistent validation pattern across all create/edit pages
- Validated fields: name, address, dates, descriptions, and image uploads
- Edit pages allow existing images, create pages require new uploads

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-18 10:51:10 +08:00
9678e6979b feat: implement form validation for empty fields across multiple admin pages
- Added validation to disable submit buttons when required fields are empty
- Implemented consistent validation patterns across various admin pages
- Applied validation to create and edit forms for berita, gallery, layanan, penghargaan, pengumuman, potensi, profil-desa, and ppid sections
- Used helper functions to check for empty HTML content in editor fields
- Ensured submit buttons are disabled until all required fields are filled

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-14 14:17:17 +08:00
b35874b120 feat: add form validation and disable submit buttons when fields are empty
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-11 17:04:55 +08:00
b69df2454e Fix Coba lagi image stagging 2026-02-05 14:29:44 +08:00
df198c320a Seed create 2026-02-05 14:03:40 +08:00
f550e29a75 Fix ke 3 Env dan seafile 2026-02-05 12:34:31 +08:00
bb7384f1e5 Fix Seed Profile PPID
Fix Seed Visi Misi Desa Profile Desa
2026-02-05 12:06:20 +08:00
df154806f7 Fix image di seafile sudah tidak pakai token tapi by folder di seafile
Kasih console di page profil ppid & visi misi di Profile Desa
2026-02-05 11:10:30 +08:00
25000d0b0f PPID > Profile PPID
Desa >  Profile Visi Misi Desa
Keamanan >  Pencegahan Kriminalitas  Grid Kiri  Datanya  Mepet
Keamanan >  Laporan  Publik
Ekonomi  >  Sektor Unggulan Desa Coba tampilin 3  Aja
2026-02-04 16:59:49 +08:00
bbd52fb6f5 Fix Jam Operasional Kantor Desa
Fix Agar Token Seafile ga expired cuma 1 hari
2026-02-04 11:47:56 +08:00
272 changed files with 8642 additions and 765 deletions

View File

@@ -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();

17
.gemini/settings.json Normal file
View File

@@ -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
}
]
}
]
}
}

3
.gitignore vendored
View File

@@ -31,6 +31,9 @@ yarn-error.log*
# env
.env*
# QC
QC
# vercel
.vercel

62
GEMINI.md Normal file
View File

@@ -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.

232
QWEN.md
View File

@@ -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

View File

@@ -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');
});
});

View File

@@ -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();
});

View File

@@ -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,
}
});
}),
];

View File

@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

7
__tests__/setup.ts Normal file
View File

@@ -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());

BIN
bun.lockb

Binary file not shown.

169
darkMode.md Normal file
View File

@@ -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 (23px 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: 1216px
- 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.

View File

@@ -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"
@@ -50,6 +53,7 @@
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
"async-mutex": "^0.5.0",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
@@ -58,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",
@@ -105,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"
}
}

View File

@@ -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]
```

File diff suppressed because one or more lines are too long

25
playwright.config.ts Normal file
View File

@@ -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,
},
});

View File

@@ -4,6 +4,7 @@ import maskotDesa from "../../../data/desa/profile/maskot_desa.json";
import profilePerbekel from "../../../data/desa/profile/profil_perbekel.json";
import profileDesaImage from "../../../data/desa/profile/profileDesaImage.json";
import sejarahDesa from "../../../data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "../../../data/desa/profile/visi_misi_desa.json";
export async function seedProfileDesa() {
// =========== SEJARAH DESA ===========
@@ -26,6 +27,26 @@ export async function seedProfileDesa() {
console.log("sejarah desa success ...");
// =========== VISI MISI DESA ===========
for (const l of visiMisiDesa) {
await prisma.visiMisiDesa.upsert({
where: {
id: l.id,
},
update: {
visi: l.visi,
misi: l.misi,
},
create: {
id: l.id,
visi: l.visi,
misi: l.misi,
},
});
}
console.log("visi misi desa success ...");
// =========== MASKOT DESA ===========
for (const l of maskotDesa) {
await prisma.maskotDesa.upsert({

View File

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

View File

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

View File

@@ -0,0 +1,246 @@
// import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
// type CdnItem = {
// name: string;
// path: string;
// cdnUrl: string;
// };
// type DirItem = {
// type: "file" | "dir";
// name: string;
// };
// const BASE_URL = process.env.SEAFILE_BASE_URL!;
// const REPO_ID = process.env.SEAFILE_REPO_ID!;
// // folder yang dishare (RELATIVE, tanpa slash depan)
// const DIR_TARGET = "asset-web";
// // 🔑 TOKEN DIRECTORY SHARE (/d/{token})
// const PUBLIC_SHARE_TOKEN = process.env.SEAFILE_PUBLIC_SHARE_TOKEN!;
// /**
// * Ambil list file dari repo (butuh token sekali)
// */
// async function getDirItems(): Promise<DirItem[]> {
// const token = await getValidAuthToken();
// // Validasi bahwa semua variabel lingkungan telah diatur
// if (!BASE_URL) {
// throw new Error('SEAFILE_BASE_URL environment variable is not set');
// }
// if (!REPO_ID) {
// throw new Error('SEAFILE_REPO_ID environment variable is not set');
// }
// // Bangun URL dan pastikan valid
// const url = `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`;
// try {
// new URL(url); // Ini akan melempar error jika URL tidak valid
// } catch (error) {
// throw new Error(`Invalid URL constructed: ${url}. Error: ${error}`);
// }
// const res = await fetch(url, {
// headers: {
// Authorization: `Token ${token}`,
// },
// });
// if (!res.ok) {
// const text = await res.text();
// throw new Error(`Failed get dir items: ${text}`);
// }
// return res.json();
// }
// /**
// * Build PUBLIC CDN URL
// */
// function buildPublicCdnUrl(fileName: string) {
// return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent(
// fileName,
// )}&raw=1`;
// }
// /**
// * Ambil semua PUBLIC CDN URL
// */
// export async function getAllPublicCdnUrls(): Promise<CdnItem[]> {
// const items = await getDirItems();
// return items
// .filter((item) => item.type === "file")
// .map((file) => {
// const path = `${DIR_TARGET}/${file.name}`;
// return {
// name: file.name,
// path,
// cdnUrl: buildPublicCdnUrl(file.name),
// };
// });
// }
// /**
// * Run langsung (optional)
// */
// if (import.meta.main) {
// const data = await getAllPublicCdnUrls();
// console.log(data);
// }
// import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
// type CdnItem = {
// name: string;
// path: string;
// cdnUrl: string;
// };
// type DirItem = {
// type: "file" | "dir";
// name: string;
// };
// // ✅ PAKAI ENV YANG BENAR
// const BASE_URL = process.env.SEAFILE_URL!;
// const REPO_ID = process.env.SEAFILE_REPO_ID!;
// const PUBLIC_SHARE_TOKEN = process.env.SEAFILE_PUBLIC_SHARE_TOKEN!;
// // folder yang dishare (RELATIVE, TANPA slash depan)
// const DIR_TARGET = "asset-web";
// /**
// * Ambil list file dari repo (token dipakai SEKALI)
// */
// async function getDirItems(): Promise<DirItem[]> {
// if (!BASE_URL || !REPO_ID) {
// throw new Error("SEAFILE env not configured correctly");
// }
// const token = await getValidAuthToken();
// const url = `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`;
// const res = await fetch(url, {
// headers: {
// Authorization: `Token ${token}`,
// },
// });
// if (!res.ok) {
// const text = await res.text();
// throw new Error(`Failed get dir items: ${text}`);
// }
// return res.json();
// }
// /**
// * Build PUBLIC CDN URL (DIRECTORY SHARE)
// */
// function buildPublicCdnUrl(fileName: string) {
// const fullPath = `/${DIR_TARGET}/${fileName}`;
// return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent(
// fullPath,
// )}&raw=1`;
// }
// /**
// * Ambil semua PUBLIC CDN URL
// */
// export async function getAllPublicCdnUrls(): Promise<CdnItem[]> {
// const items = await getDirItems();
// return items
// .filter((item) => item.type === "file")
// .map((file) => ({
// name: file.name,
// path: `${DIR_TARGET}/${file.name}`,
// cdnUrl: buildPublicCdnUrl(file.name),
// }));
// }
// /**
// * Run langsung
// */
// if (import.meta.main) {
// const data = await getAllPublicCdnUrls();
// console.log(data);
// }
import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
type CdnItem = {
name: string;
path: string;
cdnUrl: string;
};
type DirItem = {
type: "file" | "dir";
name: string;
};
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com";
const REPO_ID = process.env.SEAFILE_REPO_ID!;
// folder yang dishare (RELATIVE, tanpa slash depan)
const DIR_TARGET = "asset-web";
// 🔑 TOKEN DIRECTORY SHARE (/d/{token})
const PUBLIC_SHARE_TOKEN = "3a9a9ecb5e244f4da8ae";
/**
* Ambil list file dari repo (butuh token sekali)
*/
async function getDirItems(): Promise<DirItem[]> {
const token = await getValidAuthToken();
const res = await fetch(
`${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`,
{
headers: {
Authorization: `Token ${token}`,
},
},
);
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed get dir items: ${text}`);
}
return res.json();
}
/**
* Build PUBLIC CDN URL
*/
function buildPublicCdnUrl(fileName: string) {
return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent(
fileName,
)}&raw=1`;
}
/**
* Ambil semua PUBLIC CDN URL
*/
export async function getAllPublicCdnUrls(): Promise<CdnItem[]> {
const items = await getDirItems();
return items
.filter((item) => item.type === "file")
.map((file) => {
// const path = `${DIR_TARGET}/${file.name}`;
const path = `/${file.name}`;
return {
name: file.name,
path,
cdnUrl: buildPublicCdnUrl(file.name),
};
});
}
/**
* Run langsung (optional)
*/
if (import.meta.main) {
const data = await getAllPublicCdnUrls();
console.log(data);
}

View File

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

View File

@@ -0,0 +1,71 @@
//ini code awal cari image by folder di seafile
type CdnItem = {
name: string;
path: string;
cdnUrl: string;
};
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com";
const SHARE_ID = "3325e9db2c504ebf9584";
// https://cld-dkr-makuro-seafile.wibudev.com/d/3a9a9ecb5e244f4da8ae/
// https://cld-dkr-makuro-seafile.wibudev.com/d/3a9a9ecb5e244f4da8ae/files/?p=-M_tICRVz6ZxOfvkuHQgU-mobile.webp&raw=1
/**
* Build CDN URL langsung (tanpa API, tanpa token)
*/
export function buildCdnUrl(filePath: string) {
// filePath contoh: "banner/home.jpg"
return `${BASE_URL}/f/${SHARE_ID}/${filePath}?raw=1`;
}
/**
* Ambil daftar file dari PUBLIC SHARE (optional)
* Tidak pakai token
*/
async function getPublicDirItems(path = "/"): Promise<any[]> {
const res = await fetch(
`${BASE_URL}/api/v2.1/share-links/${SHARE_ID}/dir/?p=${encodeURIComponent(
path,
)}`,
);
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed get public dir items: ${text}`);
}
return res.json();
}
/**
* Ambil semua CDN URL dari folder public share
*/
export async function getAllCdnUrls(
dirPath = "/",
): Promise<CdnItem[]> {
const items = await getPublicDirItems(dirPath);
return items
.filter((item: any) => item.type === "file")
.map((file: any) => {
const filePath =
dirPath === "/"
? file.name
: `${dirPath.replace(/\/$/, "")}/${file.name}`;
return {
name: file.name,
path: filePath,
cdnUrl: buildCdnUrl(filePath),
};
});
}
if(import.meta.main) {
const allCdnUrls = await getAllCdnUrls();
console.log(allCdnUrls);
}

33
prisma/lib/get_sharef.ts Normal file
View File

@@ -0,0 +1,33 @@
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com";
const ADMIN_TOKEN = process.env.SEAFILE_TOKEN!;
const REPO_ID = process.env.SEAFILE_REPO_ID!;
export async function createFileShareForFolder() {
const res = await fetch(`${BASE_URL}/api/v2.1/share-links/`, {
method: "POST",
headers: {
Authorization: `Token ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
repo_id: REPO_ID,
path: "/asset-web", // FOLDER
permission: "r",
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const data = await res.json();
console.log("FILE SHARE LINK:", data);
// data.link -> https://domain/f/XXXX/
// data.token / data.id (tergantung versi)
}
if (import.meta.main) {
await createFileShareForFolder();
}

View File

@@ -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)
}

View File

@@ -65,6 +65,7 @@ import { seedBimbinganBelajar } from "./_seeder_list/pendidikan/seed_bimbingan_b
import { seedDataPendidikan } from "./_seeder_list/pendidikan/seed_data_pendidikan";
import { seedPendidikanNonFormal } from "./_seeder_list/pendidikan/seed_pendidikan_non_formal";
import { seedDataPerpustakaan } from "./_seeder_list/pendidikan/seed_data_perpustakaan";
import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd";
(async () => {
// Always run seedAssets to handle new images without duplication
@@ -179,6 +180,9 @@ import { seedDataPerpustakaan } from "./_seeder_list/pendidikan/seed_data_perpus
// // =========== MENU PPID ===========
// // =========== SUBMENU PROFIL PPID ===========
await seedProfilPpd();
// // =========== SUBMENU STRUKTUR PPID ===========
await seedPegawaiPpid();

View File

@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { getAllDownloadUrls } from "./lib/get_images";
import { getAllPublicCdnUrls } from "./lib/create_file_share_folder";
export default async function seedAssets() {
const images = await getAllDownloadUrls();
const images = await getAllPublicCdnUrls();
for (const img of images) {
try {
@@ -20,7 +20,7 @@ export default async function seedAssets() {
name: img.name,
category: "image",
mimeType: "image/webp",
link: img.downloadUrl,
link: img.cdnUrl,
path: "images",
realName: img.name,
isActive: true,
@@ -38,12 +38,12 @@ export default async function seedAssets() {
console.log("🎉 Image seeding completed");
}
// if (import.meta.main) {
// seedAssets()
// .then(() => {
// console.log("seed assets success");
// })
// .catch((err) => {
// console.log("gagal seed assets", JSON.stringify(err));
// });
// }
if (import.meta.main) {
seedAssets()
.then(() => {
console.log("seed assets success");
})
.catch((err) => {
console.log("gagal seed assets", JSON.stringify(err));
});
}

21
public/manifest.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "Desa Darmasaba",
"short_name": "Darmasaba",
"description": "Website resmi Desa Darmasaba, Kabupaten Badung, Bali",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1e40af",
"icons": [
{
"src": "/darmasaba-icon.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/darmasaba-icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -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 (
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>{title}</Title>
<UnifiedTitle order={3}>{title}</UnifiedTitle>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<Paper radius="lg" bg={tokens.colors.bg.surface}>
<TextInput
radius="lg"
placeholder={placeholder}
@@ -32,6 +39,16 @@ const HeaderSearch = ({
w="100%"
value={value}
onChange={onChange}
style={{
input: {
backgroundColor: tokens.colors.bg.surface,
color: tokens.colors.text.primary,
borderColor: tokens.colors.border.default,
'::placeholder': {
color: tokens.colors.text.muted,
},
},
}}
/>
</Paper>
</GridCol>

View File

@@ -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 (
<Grid align="center" mb={10}>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>{title}</Text>
<UnifiedText size="body" weight="bold" color="primary">{title}</UnifiedText>
</GridCol>
<GridCol span={{ base: 12, md: 1 }} ta="right">
<Button onClick={handleNavigate} bg={colors['blue-button']}>
<Button
onClick={handleNavigate}
bg={tokens.colors.primary}
style={{
background: `linear-gradient(135deg, ${tokens.colors.primary}, ${isDark ? '#60A5FA' : '#4facfe'})`,
color: tokens.colors.text.inverse,
boxShadow: isDark ? 'none' : `0 4px 15px rgba(79, 172, 254, 0.4)`,
}}
>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>

View File

@@ -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<HTMLInputElement>) => void;
}
const JudulListTab = ({
title = "",
href = "#",
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />,
value,
onChange
onChange
}: JudulListTabProps) => {
const { isDark } = useDarkMode();
const tokens = themeTokens(isDark);
const router = useRouter();
const handleNavigate = () => {
@@ -34,10 +35,17 @@ const JudulListTab = ({
return (
<Grid mb={10}>
<GridCol span={{ base: 12, md: 8 }}>
<Text fz={{ base: "md", md: "xl" }} fw={"bold"}>{title}</Text>
<UnifiedText
size="body"
weight="bold"
color="primary"
style={{ fontSize: 'clamp(1rem, 2vw, 1.25rem)' }}
>
{title}
</UnifiedText>
</GridCol>
<GridCol span={{ base: 9, md: 3 }} ta="right">
<Paper radius={"lg"} bg={colors['white-1']}>
<Paper radius={"lg"} bg={tokens.colors.bg.surface}>
<TextInput
radius="lg"
placeholder={placeholder}
@@ -45,11 +53,29 @@ const JudulListTab = ({
w="100%"
value={value}
onChange={onChange}
style={{
input: {
backgroundColor: tokens.colors.bg.surface,
color: tokens.colors.text.primary,
borderColor: tokens.colors.border.default,
'::placeholder': {
color: tokens.colors.text.muted,
},
},
}}
/>
</Paper>
</GridCol>
<GridCol span={{ base: 3, md: 1 }} ta="right">
<Button onClick={handleNavigate} bg={colors['blue-button']}>
<Button
onClick={handleNavigate}
bg={tokens.colors.primary}
style={{
background: `linear-gradient(135deg, ${tokens.colors.primary}, ${isDark ? '#60A5FA' : '#4facfe'})`,
color: tokens.colors.text.inverse,
boxShadow: isDark ? 'none' : `0 4px 15px rgba(79, 172, 254, 0.4)`,
}}
>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>

View File

@@ -38,11 +38,9 @@ function normalizeItem(item: Partial<z.infer<typeof ApbdesItemSchema>>): 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 {

View File

@@ -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

View File

@@ -57,12 +57,24 @@ const stateProfilePPID = proxy({
if (result.success) {
this.data = result.data;
return result.data;
} else throw new Error(result.message || "Gagal memuat data profile");
} else {
// Jika pesan adalah "Data tidak ditemukan" atau "Belum ada data profil PPID yang aktif",
// tetap simpan sebagai error tapi tidak perlu menampilkan toast error karena ini bukan error sebenarnya
if (result.message === "Data tidak ditemukan" || result.message === "Belum ada data profil PPID yang aktif") {
this.error = result.message;
return null;
} else {
throw new Error(result.message || "Gagal memuat data profile");
}
}
} catch (err) {
const msg = (err as Error).message;
this.error = msg;
console.error("Load profile error:", msg);
toast.error("Gagal memuat data profile");
// Hanya tampilkan toast error jika bukan karena data tidak ditemukan
if (msg !== "Data tidak ditemukan" && msg !== "Belum ada data profil PPID yang aktif") {
toast.error("Gagal memuat data profile");
}
return null;
} finally {
this.loading = false;

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -30,6 +30,22 @@ function CreateFoto() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -149,6 +149,22 @@ function EditSuratKeterangan() {
const [previewImage2, setPreviewImage2] = useState<string | null>(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<HTMLInputElement>) => {
@@ -324,10 +355,10 @@ function EditSuratKeterangan() {
{/* Action Buttons */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
<Button
variant="outline"
color="gray"
radius="md"
onClick={handleResetForm}
disabled={isSubmitting}
>
@@ -336,9 +367,11 @@ function EditSuratKeterangan() {
<Button
onClick={handleSubmit}
radius="md"
disabled={isSubmitting}
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)',
}}

View File

@@ -31,6 +31,22 @@ function CreateSuratKeterangan() {
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 (
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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -33,6 +33,23 @@ function EditPenghargaan() {
const [file, setFile] = useState<File | null>(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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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 ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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 ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -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."}
</Text>
</Paper>
</Stack>

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -56,6 +56,14 @@ export default function EditPegawaiBumDes() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// Check if form is valid
const isFormValid = () => {
return (
formData.namaLengkap?.trim() !== '' &&
formData.posisiId?.trim() !== ''
);
};
// Format date for <input type="date">
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)',
}}

View File

@@ -23,6 +23,15 @@ function CreatePegawaiBumDes() {
const [file, setFile] = useState<File | null>(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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -16,6 +16,17 @@ function CreateGrafikBerdasarkanPendidikan() {
const [donutData, setDonutData] = useState<any[]>([]);
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)',
}}

View File

@@ -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)',
}}

View File

@@ -17,6 +17,16 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [donutData, setDonutData] = useState<any[]>([]);
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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -32,6 +32,24 @@ export default function CreatePasarDesa() {
const [file, setFile] = useState<File | null>(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)',
}}

View File

@@ -53,6 +53,24 @@ function EditProgramKemiskinan() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState<FormData>(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)',
}}

View File

@@ -29,6 +29,25 @@ function CreateProgramKemiskinan() {
const [lineChart, setLineChart] = useState<any[]>([]);
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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

View File

@@ -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)',
}}

Some files were not shown because too many files have changed in this diff Show More