feat: add form validation and disable submit buttons when fields are empty
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
65
.gemini/hooks/telegram-notify.ts
Executable file
65
.gemini/hooks/telegram-notify.ts
Executable 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
17
.gemini/settings.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
62
GEMINI.md
Normal file
62
GEMINI.md
Normal 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
232
QWEN.md
@@ -0,0 +1,232 @@
|
|||||||
|
# Desa Darmasaba - Village Management System
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
|
||||||
|
|
||||||
|
### Key Technologies
|
||||||
|
- **Framework**: Next.js 15 with App Router
|
||||||
|
- **Language**: TypeScript with strict mode
|
||||||
|
- **Styling**: Mantine UI components with custom CSS
|
||||||
|
- **Backend**: Elysia.js API server integrated with Next.js
|
||||||
|
- **Database**: PostgreSQL with Prisma ORM
|
||||||
|
- **State Management**: Valtio for global state
|
||||||
|
- **Authentication**: JWT with iron-session
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
The application follows a modular architecture with:
|
||||||
|
- A main frontend built with Next.js and Mantine UI
|
||||||
|
- An integrated Elysia.js API server for backend operations
|
||||||
|
- Prisma ORM for database interactions
|
||||||
|
- File storage integration with Seafile
|
||||||
|
- Multiple domain-specific modules (PPID, health, security, education, etc.)
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js (with Bun runtime)
|
||||||
|
- PostgreSQL database
|
||||||
|
- Seafile server for file storage
|
||||||
|
|
||||||
|
### Setup Instructions
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set up environment variables in `.env.local`:
|
||||||
|
```
|
||||||
|
DATABASE_URL=your_postgresql_connection_string
|
||||||
|
SEAFILE_TOKEN=your_seafile_token
|
||||||
|
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||||
|
SEAFILE_BASE_URL=your_seafile_base_url
|
||||||
|
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||||
|
SEAFILE_URL=your_seafile_api_url
|
||||||
|
WIBU_UPLOAD_DIR=your_upload_directory
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Generate Prisma client:
|
||||||
|
```bash
|
||||||
|
bunx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Push database schema:
|
||||||
|
```bash
|
||||||
|
bunx prisma db push
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Seed the database:
|
||||||
|
```bash
|
||||||
|
bun run prisma/seed.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run the development server:
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
- `bun run dev` - Start development server
|
||||||
|
- `bun run build` - Build for production
|
||||||
|
- `bun run start` - Start production server
|
||||||
|
- `bun run prisma/seed.ts` - Run database seeding
|
||||||
|
- `bunx prisma generate` - Generate Prisma client
|
||||||
|
- `bunx prisma db push` - Push schema changes to database
|
||||||
|
- `bunx prisma studio` - Open Prisma Studio GUI
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
### Code Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Next.js app router pages
|
||||||
|
│ ├── admin/ # Admin dashboard pages
|
||||||
|
│ ├── api/ # API routes with Elysia.js
|
||||||
|
│ ├── darmasaba/ # Public-facing village pages
|
||||||
|
│ └── ...
|
||||||
|
├── con/ # Constants and configuration
|
||||||
|
├── hooks/ # React hooks
|
||||||
|
├── lib/ # Utility functions and configurations
|
||||||
|
├── middlewares/ # Next.js middleware
|
||||||
|
├── state/ # Global state management
|
||||||
|
├── store/ # Additional state management
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
└── utils/ # Utility functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Conventions
|
||||||
|
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||||
|
- Group imports: external libraries first, then internal modules
|
||||||
|
- Keep import statements organized and remove unused imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// External libraries
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button, Stack } from '@mantine/core'
|
||||||
|
|
||||||
|
// Internal modules
|
||||||
|
import ApiFetch from '@/lib/api-fetch'
|
||||||
|
import { MyComponent } from '@/components/my-component'
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript Configuration
|
||||||
|
- Strict mode enabled (`"strict": true`)
|
||||||
|
- Target: ES2017
|
||||||
|
- Module resolution: bundler
|
||||||
|
- Path alias: `@/*` maps to `./src/*`
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
||||||
|
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
||||||
|
- **Variables/Functions**: camelCase
|
||||||
|
- **Constants**: UPPER_SNAKE_CASE
|
||||||
|
- **Database Models**: PascalCase (Prisma convention)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use try-catch blocks for async operations
|
||||||
|
- Implement proper error boundaries in React components
|
||||||
|
- Log errors appropriately without exposing sensitive data
|
||||||
|
- Use Zod for runtime validation and type safety
|
||||||
|
|
||||||
|
### API Structure
|
||||||
|
- Backend uses Elysia.js with TypeScript
|
||||||
|
- API routes are in `src/app/api/[[...slugs]]/` directory
|
||||||
|
- Use treaty client for type-safe API calls
|
||||||
|
- Follow RESTful conventions for endpoints
|
||||||
|
- Include proper HTTP status codes and error responses
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
- Use Prisma client from `@/lib/prisma.ts`
|
||||||
|
- Database connection includes graceful shutdown handling
|
||||||
|
- Use transactions for complex operations
|
||||||
|
- Implement proper error handling for database queries
|
||||||
|
|
||||||
|
### Component Guidelines
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Implement proper prop types with TypeScript interfaces
|
||||||
|
- Use Mantine components for UI consistency
|
||||||
|
- Follow atomic design principles when possible
|
||||||
|
- Add loading states and error states for async operations
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- Use Valtio proxies for global state
|
||||||
|
- Keep local state in components when possible
|
||||||
|
- Use SWR for server state caching
|
||||||
|
- Implement optimistic updates for better UX
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
- Primary: Mantine UI components
|
||||||
|
- Use Mantine theme system for customization
|
||||||
|
- Custom CSS should be minimal and scoped
|
||||||
|
- Follow responsive design principles
|
||||||
|
- Use semantic HTML5 elements
|
||||||
|
|
||||||
|
### Security Practices
|
||||||
|
- Validate all user inputs with Zod schemas
|
||||||
|
- Use JWT tokens for authentication
|
||||||
|
- Implement proper CORS configuration
|
||||||
|
- Never expose database credentials or API keys
|
||||||
|
- Use HTTPS in production
|
||||||
|
- Implement rate limiting for sensitive endpoints
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- Use Next.js Image optimization
|
||||||
|
- Implement proper caching strategies
|
||||||
|
- Use React.memo for expensive components
|
||||||
|
- Optimize bundle size with dynamic imports
|
||||||
|
- Use Prisma query optimization
|
||||||
|
|
||||||
|
## Domain Modules
|
||||||
|
|
||||||
|
The application is organized into several domain modules:
|
||||||
|
|
||||||
|
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
|
||||||
|
2. **Health**: Health facilities, programs, emergency response, disease information
|
||||||
|
3. **Security**: Community security, emergency contacts, crime prevention
|
||||||
|
4. **Education**: Schools, scholarships, educational programs
|
||||||
|
5. **Economy**: Local markets, BUMDes, employment data
|
||||||
|
6. **Environment**: Environmental data, conservation, waste management
|
||||||
|
7. **Innovation**: Digital services, innovation programs
|
||||||
|
8. **Culture**: Village traditions, music, cultural preservation
|
||||||
|
|
||||||
|
Each module has its own section in both the admin panel and public-facing areas.
|
||||||
|
|
||||||
|
## File Storage Integration
|
||||||
|
|
||||||
|
The application integrates with Seafile for file storage, with specific handling for:
|
||||||
|
- Images and documents
|
||||||
|
- Public sharing capabilities
|
||||||
|
- CDN URL generation
|
||||||
|
- Batch processing of assets
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Currently no formal test framework is configured. When adding tests:
|
||||||
|
- Consider Jest or Vitest for unit testing
|
||||||
|
- Use Playwright for E2E testing
|
||||||
|
- Update this section with specific test commands
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The application includes deployment scripts in the `NOTE.md` file that outline:
|
||||||
|
- Automated deployment with GitHub API integration
|
||||||
|
- Environment-specific configurations
|
||||||
|
- PM2 process management
|
||||||
|
- Release management with versioning
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Common issues and solutions:
|
||||||
|
- **API endpoints returning 404**: Check that environment variables are properly configured
|
||||||
|
- **Database connection errors**: Verify DATABASE_URL in environment variables
|
||||||
|
- **File upload issues**: Ensure Seafile integration is properly configured
|
||||||
|
- **Build failures**: Run `bunx prisma generate` before building
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. Always run type checking before committing: `bunx tsc --noEmit`
|
||||||
|
2. Run linting to catch style issues: `bun run eslint .`
|
||||||
|
3. Test database changes with `bunx prisma db push`
|
||||||
|
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||||
|
5. Check environment variables are properly configured
|
||||||
|
6. Verify responsive design on different screen sizes
|
||||||
30
__tests__/api/fileStorage.test.ts
Normal file
30
__tests__/api/fileStorage.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
11
__tests__/e2e/homepage.spec.ts
Normal file
11
__tests__/e2e/homepage.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
43
__tests__/mocks/handlers.ts
Normal file
43
__tests__/mocks/handlers.ts
Normal 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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
];
|
||||||
4
__tests__/mocks/server.ts
Normal file
4
__tests__/mocks/server.ts
Normal 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
7
__tests__/setup.ts
Normal 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());
|
||||||
13
package.json
13
package.json
@@ -5,7 +5,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"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": {
|
"prisma": {
|
||||||
"seed": "bun run prisma/seed.ts"
|
"seed": "bun run prisma/seed.ts"
|
||||||
@@ -106,17 +109,23 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@types/cli-progress": "^3.11.6",
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@vitest/ui": "^4.0.18",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.6",
|
"eslint-config-next": "15.1.6",
|
||||||
|
"jsdom": "^28.0.0",
|
||||||
|
"msw": "^2.12.9",
|
||||||
"parcel": "^2.6.2",
|
"parcel": "^2.6.2",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
```
|
||||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
25
playwright.config.ts
Normal file
25
playwright.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -37,6 +37,15 @@ export default function EditKolaborasiInovasi() {
|
|||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.jumlah?.trim() !== '' &&
|
||||||
|
(formData.imageId?.trim() !== '' || file !== null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
jumlah: "",
|
jumlah: "",
|
||||||
@@ -252,8 +261,11 @@ export default function EditKolaborasiInovasi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ function CreateSDGsDesa() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateSDGSDesa.create.form.name?.trim() !== '' &&
|
||||||
|
stateSDGSDesa.create.form.jumlah?.trim() !== '' &&
|
||||||
|
file !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateSDGSDesa.findMany.load();
|
stateSDGSDesa.findMany.load();
|
||||||
@@ -203,8 +211,11 @@ function CreateSDGsDesa() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ function EditAPBDes() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
apbdesState.edit.form.items.length > 0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
|
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
|
||||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||||
@@ -492,9 +500,11 @@ function EditAPBDes() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
disabled={apbdesState.edit.form.items.length === 0 || apbdesState.edit.loading}
|
disabled={!isFormValid() || isSubmitting || apbdesState.edit.loading}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting || apbdesState.edit.loading
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ function CreateAPBDes() {
|
|||||||
const [docFile, setDocFile] = useState<File | null>(null);
|
const [docFile, setDocFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
imageFile !== null &&
|
||||||
|
docFile !== null &&
|
||||||
|
stateAPBDes.create.form.items.length > 0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Form sementara untuk input item baru
|
// Form sementara untuk input item baru
|
||||||
const [newItem, setNewItem] = useState<ItemForm>({
|
const [newItem, setNewItem] = useState<ItemForm>({
|
||||||
kode: '',
|
kode: '',
|
||||||
@@ -468,9 +477,11 @@ function CreateAPBDes() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
disabled={stateAPBDes.create.form.items.length === 0}
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export default function EditKategoriDesaAntiKorupsi() {
|
|||||||
const [originalData, setOriginalData] = useState({ name: '' });
|
const [originalData, setOriginalData] = useState({ name: '' });
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return formData.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
// 📥 load data saat pertama kali dibuka
|
// 📥 load data saat pertama kali dibuka
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -126,9 +131,11 @@ export default function EditKategoriDesaAntiKorupsi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
disabled={isSubmitting}
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export default function CreateKategoriDesaAntiKorupsi() {
|
|||||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return stateKategori.create.form.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateKategori.findMany.load();
|
stateKategori.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -87,8 +92,11 @@ export default function CreateKategoriDesaAntiKorupsi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ export default function EditDesaAntiKorupsi() {
|
|||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.kategoriId?.trim() !== '' &&
|
||||||
|
(formData.fileId?.trim() !== '' || file !== null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
@@ -287,8 +296,11 @@ export default function EditDesaAntiKorupsi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ export default function CreateDesaAntiKorupsi() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
file !== null &&
|
||||||
|
stateKorupsi.create.form.name?.trim() !== '' &&
|
||||||
|
stateKorupsi.create.form.kategoriId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateKorupsi.findMany.load();
|
stateKorupsi.findMany.load();
|
||||||
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
|
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
|
||||||
@@ -238,8 +247,11 @@ export default function CreateDesaAntiKorupsi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ function EditKategoriPrestasi() {
|
|||||||
const [resetData, setResetData] = useState({ name: '' });
|
const [resetData, setResetData] = useState({ name: '' });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return formData.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
// Load data kategori prestasi saat component mount
|
// Load data kategori prestasi saat component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -123,8 +128,11 @@ function EditKategoriPrestasi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ function CreateKategoriPrestasi() {
|
|||||||
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
|
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return stateKategori.create.form.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateKategori.findMany.load();
|
stateKategori.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -84,8 +89,11 @@ function CreateKategoriPrestasi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ export default function EditPrestasiDesa() {
|
|||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.kategoriId?.trim() !== '' &&
|
||||||
|
(formData.imageId?.trim() !== '' || file !== null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
@@ -253,8 +262,11 @@ export default function EditPrestasiDesa() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ function CreatePrestasiDesa() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
file !== null &&
|
||||||
|
snapPrestasi.create.form.name?.trim() !== '' &&
|
||||||
|
snapPrestasi.create.form.kategoriId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
prestasi.findMany.load();
|
prestasi.findMany.load();
|
||||||
@@ -226,8 +234,11 @@ function CreatePrestasiDesa() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ function EditMediaSosial() {
|
|||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
const isNameValid = formData.name?.trim() !== '';
|
||||||
|
const isIconUrlValid = formData.iconUrl?.trim() !== '';
|
||||||
|
const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
|
||||||
|
|
||||||
|
return isNameValid && isIconUrlValid && isCustomIconValid;
|
||||||
|
};
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
icon: '',
|
icon: '',
|
||||||
@@ -340,8 +349,11 @@ function EditMediaSosial() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||||
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
||||||
|
import { sosmedMap } from '@/app/admin/(dashboard)/landing-page/profil/_lib/sosmed';
|
||||||
|
|
||||||
// ⭐ Tambah type SosmedKey
|
// ⭐ Tambah type SosmedKey
|
||||||
type SosmedKey =
|
type SosmedKey =
|
||||||
@@ -38,20 +39,6 @@ type SosmedKey =
|
|||||||
| 'telephone'
|
| 'telephone'
|
||||||
| 'custom';
|
| 'custom';
|
||||||
|
|
||||||
// ⭐ mapping icon sosmed bawaan
|
|
||||||
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CreateMediaSosial() {
|
export default function CreateMediaSosial() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
@@ -61,6 +48,15 @@ export default function CreateMediaSosial() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
const isNameValid = stateMediaSosial.create.form.name?.trim() !== '';
|
||||||
|
const isIconUrlValid = stateMediaSosial.create.form.iconUrl?.trim() !== '';
|
||||||
|
const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
|
||||||
|
|
||||||
|
return isNameValid && isIconUrlValid && isCustomIconValid;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateMediaSosial.findMany.load();
|
stateMediaSosial.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -85,7 +81,7 @@ export default function CreateMediaSosial() {
|
|||||||
// ──────────────── ⭐ CASE 1: PAKAI ICON DEFAULT ────────────────
|
// ──────────────── ⭐ CASE 1: PAKAI ICON DEFAULT ────────────────
|
||||||
if (selectedSosmed !== 'custom') {
|
if (selectedSosmed !== 'custom') {
|
||||||
stateMediaSosial.create.form.imageId = null;
|
stateMediaSosial.create.form.imageId = null;
|
||||||
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
|
stateMediaSosial.create.form.icon = selectedSosmed; // Store the key, not the path
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
await stateMediaSosial.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
@@ -261,8 +257,11 @@ export default function CreateMediaSosial() {
|
|||||||
<Button
|
<Button
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ function EditPejabatDesa() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.position?.trim() !== '' &&
|
||||||
|
(formData.imageId?.trim() !== '' || file !== null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data on mount
|
// Load data on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -327,11 +336,13 @@ function EditPejabatDesa() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={isSubmitting || allState.edit.loading}
|
loading={isSubmitting || allState.edit.loading}
|
||||||
disabled={!formData.name}
|
disabled={!isFormValid() || isSubmitting || allState.edit.loading}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting || allState.edit.loading
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ function EditProgramInovasi() {
|
|||||||
})
|
})
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.description?.trim() !== '' &&
|
||||||
|
formData.link?.trim() !== '' &&
|
||||||
|
(formData.imageId?.trim() !== '' || file !== null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -271,8 +281,11 @@ function EditProgramInovasi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ function CreateProgramInovasi() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateProgramInovasi.create.form.name?.trim() !== '' &&
|
||||||
|
stateProgramInovasi.create.form.description?.trim() !== '' &&
|
||||||
|
stateProgramInovasi.create.form.link?.trim() !== '' &&
|
||||||
|
file !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateProgramInovasi.findMany.load();
|
stateProgramInovasi.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -221,8 +231,11 @@ function CreateProgramInovasi() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
|
disabled={!isFormValid() || isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || isSubmitting
|
||||||
|
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
28
test-berita-state.ts
Normal file
28
test-berita-state.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import stateDashboardBerita from './src/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
|
||||||
|
// Simple validation to check if the state structure is correct
|
||||||
|
console.log('Testing Berita State Structure...');
|
||||||
|
|
||||||
|
// Check if main objects exist
|
||||||
|
console.log('✓ Berita object exists:', !!stateDashboardBerita.berita);
|
||||||
|
console.log('✓ KategoriBerita object exists:', !!stateDashboardBerita.kategoriBerita);
|
||||||
|
|
||||||
|
// Check if required methods exist in berita
|
||||||
|
const berita = stateDashboardBerita.berita;
|
||||||
|
console.log('✓ Berita.create exists:', !!berita.create);
|
||||||
|
console.log('✓ Berita.findMany exists:', !!berita.findMany);
|
||||||
|
console.log('✓ Berita.findUnique exists:', !!berita.findUnique);
|
||||||
|
console.log('✓ Berita.delete exists:', !!berita.delete);
|
||||||
|
console.log('✓ Berita.edit exists:', !!berita.edit);
|
||||||
|
console.log('✓ Berita.findFirst exists:', !!berita.findFirst);
|
||||||
|
console.log('✓ Berita.findRecent exists:', !!berita.findRecent);
|
||||||
|
|
||||||
|
// Check if required methods exist in kategoriBerita
|
||||||
|
const kategoriBerita = stateDashboardBerita.kategoriBerita;
|
||||||
|
console.log('✓ KategoriBerita.create exists:', !!kategoriBerita.create);
|
||||||
|
console.log('✓ KategoriBerita.findMany exists:', !!kategoriBerita.findMany);
|
||||||
|
console.log('✓ KategoriBerita.findUnique exists:', !!kategoriBerita.findUnique);
|
||||||
|
console.log('✓ KategoriBerita.delete exists:', !!kategoriBerita.delete);
|
||||||
|
console.log('✓ KategoriBerita.update exists:', !!kategoriBerita.update);
|
||||||
|
|
||||||
|
console.log('\\nAll state properties are correctly defined!');
|
||||||
6
test-results/.last-run.json
Normal file
6
test-results/.last-run.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"status": "failed",
|
||||||
|
"failedTests": [
|
||||||
|
"84e7eb052e8cbae1c9f0-6067b767debcd3745af5"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
```
|
||||||
25
types/env.d.ts
vendored
25
types/env.d.ts
vendored
@@ -1,15 +1,38 @@
|
|||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
|
// Database
|
||||||
DATABASE_URL?: string;
|
DATABASE_URL?: string;
|
||||||
|
|
||||||
|
// Seafile Configuration
|
||||||
SEAFILE_TOKEN?: string;
|
SEAFILE_TOKEN?: string;
|
||||||
SEAFILE_REPO_ID?: string;
|
SEAFILE_REPO_ID?: string;
|
||||||
|
SEAFILE_BASE_URL?: string;
|
||||||
|
SEAFILE_PUBLIC_SHARE_TOKEN?: string;
|
||||||
|
SEAFILE_URL?: string;
|
||||||
|
|
||||||
|
// Upload/Download Directories
|
||||||
WIBU_UPLOAD_DIR?: string;
|
WIBU_UPLOAD_DIR?: string;
|
||||||
WIBU_DOWNLOAD_DIR?: string;
|
WIBU_DOWNLOAD_DIR?: string;
|
||||||
|
|
||||||
|
// Email Configuration
|
||||||
EMAIL_USER?: string;
|
EMAIL_USER?: string;
|
||||||
EMAIL_PASS?: string;
|
EMAIL_PASS?: string;
|
||||||
|
|
||||||
|
// Application URLs
|
||||||
NEXT_PUBLIC_BASE_URL?: string;
|
NEXT_PUBLIC_BASE_URL?: string;
|
||||||
|
|
||||||
|
// Authentication
|
||||||
NEXTAUTH_SECRET?: string;
|
NEXTAUTH_SECRET?: string;
|
||||||
NEXTAUTH_URL?: string;
|
NEXTAUTH_URL?: string;
|
||||||
SEAFILE_URL?: string;
|
BASE_SESSION_KEY?: string;
|
||||||
|
BASE_TOKEN_KEY?: string;
|
||||||
|
NEXT_PUBLIC_BASE_SESSION_KEY?: string;
|
||||||
|
NEXT_PUBLIC_BASE_TOKEN_KEY?: string;
|
||||||
|
|
||||||
|
// API Keys
|
||||||
|
ELEVENLABS_API_KEY?: string;
|
||||||
|
|
||||||
|
// Environment
|
||||||
|
NODE_ENV?: 'development' | 'production' | 'test';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
vitest.config.ts
Normal file
15
vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './__tests__/setup.ts',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user