Compare commits
6 Commits
nico/fix-p
...
fix/admin/
| Author | SHA1 | Date | |
|---|---|---|---|
| 22de1aa1f3 | |||
| b1d28a8322 | |||
| b86a3a85c3 | |||
| fd63bb0fd4 | |||
| f0558aa0d0 | |||
| 8132609ccb |
169
darkMode.md
Normal file
169
darkMode.md
Normal 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 (2–3px accent bar)
|
||||||
|
- Hover:
|
||||||
|
- Background: `rgba(255,255,255,0.04)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Header / Topbar
|
||||||
|
- Background: `linear-gradient(#0F172A → #0B1220)`
|
||||||
|
- Border bawah wajib (`--border-soft`)
|
||||||
|
- Icon:
|
||||||
|
- Default: muted
|
||||||
|
- Hover: primary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Card & Section
|
||||||
|
|
||||||
|
### Card
|
||||||
|
- Background: `--bg-card`
|
||||||
|
- Border: `--border-default`
|
||||||
|
- Radius: 12–16px
|
||||||
|
- Jangan pakai shadow hitam
|
||||||
|
|
||||||
|
### Section Header
|
||||||
|
- Font weight lebih besar
|
||||||
|
- Text: primary
|
||||||
|
- Spacing jelas dari konten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Table (Dark Mode Friendly)
|
||||||
|
|
||||||
|
### Table Header
|
||||||
|
- Background: `--bg-surface`
|
||||||
|
- Text: secondary
|
||||||
|
- Font weight: medium
|
||||||
|
|
||||||
|
### Table Row
|
||||||
|
- Default: transparent
|
||||||
|
- Hover:
|
||||||
|
- Background: `rgba(255,255,255,0.03)`
|
||||||
|
- Divider antar row wajib terlihat
|
||||||
|
|
||||||
|
### Link di Table
|
||||||
|
- Warna link **lebih terang dari text**
|
||||||
|
- Hover underline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔘 Button Rules
|
||||||
|
|
||||||
|
### Primary Button
|
||||||
|
- Background: Primary Action
|
||||||
|
- Text: Inverse
|
||||||
|
- Hover: darker shade
|
||||||
|
|
||||||
|
### Secondary Button
|
||||||
|
- Background: transparent
|
||||||
|
- Border: `--border-default`
|
||||||
|
- Text: primary
|
||||||
|
|
||||||
|
### Icon Button
|
||||||
|
- Default: muted
|
||||||
|
- Hover: primary + bg soft
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧭 Tab Navigation
|
||||||
|
|
||||||
|
- Inactive:
|
||||||
|
- Text: muted
|
||||||
|
- Active:
|
||||||
|
- Background: `rgba(59,130,246,0.15)`
|
||||||
|
- Text: primary
|
||||||
|
- Icon ikut berubah
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌗 Dark vs Light Mode Rule
|
||||||
|
- Layout, spacing, typography **HARUS SAMA**
|
||||||
|
- Yang boleh beda:
|
||||||
|
- warna
|
||||||
|
- border intensity
|
||||||
|
- background layer
|
||||||
|
|
||||||
|
> ❌ Jangan ganti struktur UI antara dark & light
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Dark Mode Checklist
|
||||||
|
- [ ] Kontras teks terbaca
|
||||||
|
- [ ] Active state jelas
|
||||||
|
- [ ] Hover terasa hidup
|
||||||
|
- [ ] Tidak flat
|
||||||
|
- [ ] Tidak silau
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Dokumen ini adalah **single source of truth** untuk Dark Mode.
|
||||||
@@ -236,7 +236,7 @@ model PrestasiDesa {
|
|||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ model KategoriPrestasiDesa {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PrestasiDesa PrestasiDesa[]
|
PrestasiDesa PrestasiDesa[]
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,7 @@ model Responden {
|
|||||||
kelompokUmurId String
|
kelompokUmurId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ model JenisKelaminResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,7 @@ model PilihanRatingResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ model UmurResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -326,6 +326,7 @@ model PosisiOrganisasiPPID {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
@@ -345,6 +346,7 @@ model PegawaiPPID {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
@@ -370,7 +372,7 @@ model VisiMisiPPID {
|
|||||||
misi String @db.Text
|
misi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +383,7 @@ model DasarHukumPPID {
|
|||||||
content String @db.Text
|
content String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +400,7 @@ model ProfilePPID {
|
|||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +412,7 @@ model DaftarInformasiPublik {
|
|||||||
tanggal DateTime @db.Date
|
tanggal DateTime @db.Date
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +433,7 @@ model PermohonanInformasiPublik {
|
|||||||
caraMemperolehSalinanInformasiId String?
|
caraMemperolehSalinanInformasiId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +442,7 @@ model JenisInformasiDiminta {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -450,7 +452,7 @@ model CaraMemperolehInformasi {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -460,7 +462,7 @@ model CaraMemperolehSalinanInformasi {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -474,7 +476,7 @@ model FormulirPermohonanKeberatan {
|
|||||||
alasan String
|
alasan String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +533,7 @@ model SejarahDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +543,7 @@ model VisiMisiDesa {
|
|||||||
misi String @db.Text
|
misi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +553,7 @@ model LambangDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +564,7 @@ model MaskotDesa {
|
|||||||
images ProfileDesaImage[]
|
images ProfileDesaImage[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,25 +633,25 @@ model KategoriBerita {
|
|||||||
// ========================================= POTENSI DESA ========================================= //
|
// ========================================= POTENSI DESA ========================================= //
|
||||||
model PotensiDesa {
|
model PotensiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String @unique @db.VarChar(255)
|
||||||
deskripsi String
|
deskripsi String @db.Text
|
||||||
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
|
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String?
|
kategoriId String @db.VarChar(36)
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
content String @db.Text
|
content String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model KategoriPotensi {
|
model KategoriPotensi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
nama String
|
nama String @unique @db.VarChar(100)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PotensiDesa PotensiDesa[]
|
PotensiDesa PotensiDesa[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd";
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// Always run seedAssets to handle new images without duplication
|
// Always run seedAssets to handle new images without duplication
|
||||||
// console.log("📂 Checking for new assets to seed...");
|
console.log("📂 Checking for new assets to seed...");
|
||||||
// await seedAssets();
|
await seedAssets();
|
||||||
|
|
||||||
// // =========== FILE STORAGE ===========
|
// // =========== FILE STORAGE ===========
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
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 { 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 = {
|
type HeaderSearchProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -18,13 +22,16 @@ const HeaderSearch = ({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: HeaderSearchProps) => {
|
}: HeaderSearchProps) => {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid mb={10}>
|
<Grid mb={10}>
|
||||||
<GridCol span={{ base: 12, md: 9 }}>
|
<GridCol span={{ base: 12, md: 9 }}>
|
||||||
<Title order={3}>{title}</Title>
|
<UnifiedTitle order={3}>{title}</UnifiedTitle>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 3 }}>
|
<GridCol span={{ base: 12, md: 3 }}>
|
||||||
<Paper radius="lg" bg={colors['white-1']}>
|
<Paper radius="lg" bg={tokens.colors.bg.surface}>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius="lg"
|
radius="lg"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -32,6 +39,16 @@ const HeaderSearch = ({
|
|||||||
w="100%"
|
w="100%"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
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>
|
</Paper>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import { Grid, GridCol, Button } from '@mantine/core';
|
||||||
import { Grid, GridCol, Button, Text } from '@mantine/core';
|
|
||||||
import { IconCircleDashedPlus } from '@tabler/icons-react';
|
import { IconCircleDashedPlus } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react';
|
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 JudulList = ({ title = "", href = "#" }) => {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleNavigate = () => {
|
const handleNavigate = () => {
|
||||||
@@ -16,10 +20,18 @@ const JudulList = ({ title = "", href = "#" }) => {
|
|||||||
return (
|
return (
|
||||||
<Grid align="center" mb={10}>
|
<Grid align="center" mb={10}>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Text fz={"xl"} fw={"bold"}>{title}</Text>
|
<UnifiedText size="body" weight="bold" color="primary">{title}</UnifiedText>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }} ta="right">
|
<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} />
|
<IconCircleDashedPlus size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import { Grid, GridCol, Button, Paper, TextInput } from '@mantine/core';
|
||||||
import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core';
|
|
||||||
import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
import { UnifiedText } from '@/components/admin/UnifiedTypography';
|
||||||
|
|
||||||
type JudulListTabProps = {
|
type JudulListTabProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -14,17 +16,16 @@ type JudulListTabProps = {
|
|||||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const JudulListTab = ({
|
const JudulListTab = ({
|
||||||
title = "",
|
title = "",
|
||||||
href = "#",
|
href = "#",
|
||||||
placeholder = "pencarian",
|
placeholder = "pencarian",
|
||||||
searchIcon = <IconSearch size={20} />,
|
searchIcon = <IconSearch size={20} />,
|
||||||
value,
|
value,
|
||||||
onChange
|
onChange
|
||||||
}: JudulListTabProps) => {
|
}: JudulListTabProps) => {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleNavigate = () => {
|
const handleNavigate = () => {
|
||||||
@@ -34,10 +35,17 @@ const JudulListTab = ({
|
|||||||
return (
|
return (
|
||||||
<Grid mb={10}>
|
<Grid mb={10}>
|
||||||
<GridCol span={{ base: 12, md: 8 }}>
|
<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>
|
||||||
<GridCol span={{ base: 9, md: 3 }} ta="right">
|
<GridCol span={{ base: 9, md: 3 }} ta="right">
|
||||||
<Paper radius={"lg"} bg={colors['white-1']}>
|
<Paper radius={"lg"} bg={tokens.colors.bg.surface}>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius="lg"
|
radius="lg"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -45,11 +53,29 @@ const JudulListTab = ({
|
|||||||
w="100%"
|
w="100%"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
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>
|
</Paper>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 3, md: 1 }} ta="right">
|
<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} />
|
<IconCircleDashedPlus size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
export default function DetailPotensi() {
|
export default function DetailPotensi() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -77,7 +78,17 @@ export default function DetailPotensi() {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}></Text>
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(data.deskripsi || '-', {
|
||||||
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
></Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -102,7 +113,12 @@ export default function DetailPotensi() {
|
|||||||
<Text
|
<Text
|
||||||
fz="md"
|
fz="md"
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(data.content || '-', {
|
||||||
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
})
|
||||||
|
}}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
function Potensi() {
|
function Potensi() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -137,7 +138,12 @@ function ListPotensi({ search }: { search: string }) {
|
|||||||
fz="sm"
|
fz="sm"
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
lineClamp={2}
|
lineClamp={2}
|
||||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(item.deskripsi, {
|
||||||
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
})
|
||||||
|
}}
|
||||||
style={{ wordBreak: 'break-word' }}
|
style={{ wordBreak: 'break-word' }}
|
||||||
/>
|
/>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -199,7 +205,12 @@ function ListPotensi({ search }: { search: string }) {
|
|||||||
<Text
|
<Text
|
||||||
fz="sm"
|
fz="sm"
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(item.deskripsi, {
|
||||||
|
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
})
|
||||||
|
}}
|
||||||
style={{ wordBreak: 'break-word' }}
|
style={{ wordBreak: 'break-word' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function Page() {
|
|||||||
fz={{ base: 'md', md: 'lg' }}
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
lh={{ base: 1.4, md: 1.4 }}
|
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>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ function EditDigitalSmartVillage() {
|
|||||||
imageUrl: '',
|
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(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -248,8 +263,11 @@ function EditDigitalSmartVillage() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ export default function CreateDesaDigital() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
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 = () => {
|
const resetForm = () => {
|
||||||
stateDesaDigital.create.form = {
|
stateDesaDigital.create.form = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -227,8 +243,11 @@ export default function CreateDesaDigital() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ function EditInfoTeknologiTepatGuna() {
|
|||||||
imageUrl: '',
|
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
|
// Load data pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -260,8 +275,11 @@ function EditInfoTeknologiTepatGuna() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ function CreateInfoTeknologiTepatGuna() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
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 = () => {
|
const resetForm = () => {
|
||||||
stateInfoTekno.create.form = {
|
stateInfoTekno.create.form = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -202,8 +218,11 @@ function CreateInfoTeknologiTepatGuna() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -44,6 +44,23 @@ function EditKolaborasiInovasi() {
|
|||||||
kolaborator: "",
|
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
|
// Load data awal dari server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKolaborasi = async () => {
|
const loadKolaborasi = async () => {
|
||||||
@@ -199,8 +216,11 @@ 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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,6 +16,22 @@ function CreateProgramKreatifDesa() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
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 = () => {
|
const resetForm = () => {
|
||||||
stateCreate.create.form = {
|
stateCreate.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -135,8 +151,11 @@ function CreateProgramKreatifDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ function EditMitraKolaborasi() {
|
|||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return formData.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
// Load data ke state lokal sekali saja
|
// Load data ke state lokal sekali saja
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -263,8 +268,11 @@ function EditMitraKolaborasi() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ function CreateMitraKolaborasi() {
|
|||||||
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 (
|
||||||
|
state.create.form.name?.trim() !== '' &&
|
||||||
|
file !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
state.create.form = {
|
state.create.form = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -181,8 +189,11 @@ function CreateMitraKolaborasi() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,6 +51,23 @@ function EditProgramKreatifDesa() {
|
|||||||
|
|
||||||
const [isDataChanged, setIsDataChanged] = useState(false);
|
const [isDataChanged, setIsDataChanged] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.slug?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi) &&
|
||||||
|
formData.icon?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data hanya sekali berdasarkan params.id
|
// Load data hanya sekali berdasarkan params.id
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProgramKreatif = async () => {
|
const loadProgramKreatif = async () => {
|
||||||
@@ -236,8 +253,11 @@ function EditProgramKreatifDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ function CreateProgramKreatifDesa() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.name?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.icon?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.slug?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(stateCreate.create.form.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateCreate.create.form = {
|
stateCreate.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -127,8 +144,11 @@ function CreateProgramKreatifDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -67,6 +67,23 @@ export default function EditDataLingkunganDesa() {
|
|||||||
icon: '',
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.jumlah?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi) &&
|
||||||
|
formData.icon?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data saat komponen mount
|
// Load data saat komponen mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -211,8 +228,11 @@ export default function EditDataLingkunganDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ function CreateDataLingkunganDesa() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.name?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.icon?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.jumlah?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(stateCreate.create.form.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateCreate.create.form = {
|
stateCreate.create.form = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -129,8 +146,11 @@ function CreateDataLingkunganDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ export default function EditContohKegiatanDesaDarmasaba() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
!isHtmlEmpty(formData.judul) &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// load data awal
|
// load data awal
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!contohEdukasiState.findById.data) {
|
if (!contohEdukasiState.findById.data) {
|
||||||
@@ -156,8 +171,11 @@ export default function EditContohKegiatanDesaDarmasaba() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -27,6 +27,21 @@ export default function EditMateriEdukasiYangDiberikan() {
|
|||||||
content: '',
|
content: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
!isHtmlEmpty(formData.judul) &&
|
||||||
|
!isHtmlEmpty(formData.content)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize data kalau belum ada
|
// Initialize data kalau belum ada
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!materiEdukasiState.findById.data) {
|
if (!materiEdukasiState.findById.data) {
|
||||||
@@ -139,8 +154,11 @@ export default function EditMateriEdukasiYangDiberikan() {
|
|||||||
onClick={submit}
|
onClick={submit}
|
||||||
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, #eeeeee)`
|
||||||
|
: `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,6 +28,21 @@ export default function EditTujuanEdukasiLingkungan() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
!isHtmlEmpty(formData.judul) &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize global state
|
// Initialize global state
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!tujuanEdukasiState.findById.data) {
|
if (!tujuanEdukasiState.findById.data) {
|
||||||
@@ -147,8 +162,11 @@ export default function EditTujuanEdukasiLingkungan() {
|
|||||||
onClick={submit}
|
onClick={submit}
|
||||||
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, #eeeeee)`
|
||||||
|
: `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 EditKategoriKegiatan() {
|
|||||||
const [originalData, setOriginalData] = useState({ nama: '' });
|
const [originalData, setOriginalData] = useState({ nama: '' });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return formData.nama?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
// Load data once
|
// Load data once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -126,8 +131,11 @@ function EditKategoriKegiatan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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 @@ function CreateKategoriKegiatan() {
|
|||||||
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan)
|
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return stateKategori.create.form.nama?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateKategori.findMany.load();
|
stateKategori.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -84,8 +89,11 @@ function CreateKategoriKegiatan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -67,6 +67,27 @@ export default function EditKegiatanDesa() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | 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 (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsiSingkat) &&
|
||||||
|
!isHtmlEmpty(formData.deskripsiLengkap) &&
|
||||||
|
formData.tanggal?.trim() !== '' &&
|
||||||
|
formData.lokasi?.trim() !== '' &&
|
||||||
|
formData.partisipan !== null &&
|
||||||
|
formData.partisipan >= 0 &&
|
||||||
|
formData.kategoriKegiatanId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const formatDateForInput = (dateString: string) => {
|
const formatDateForInput = (dateString: string) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
return new Date(dateString).toISOString().split('T')[0];
|
return new Date(dateString).toISOString().split('T')[0];
|
||||||
@@ -312,8 +333,11 @@ export default function EditKegiatanDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,6 +38,28 @@ function CreateKegiatanDesa() {
|
|||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateKegiatanDesa.create.form.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(stateKegiatanDesa.create.form.deskripsiSingkat) &&
|
||||||
|
stateKegiatanDesa.create.form.partisipan !== null &&
|
||||||
|
stateKegiatanDesa.create.form.partisipan >= 0 &&
|
||||||
|
stateKegiatanDesa.create.form.tanggal !== null &&
|
||||||
|
stateKegiatanDesa.create.form.lokasi?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(stateKegiatanDesa.create.form.deskripsiLengkap) &&
|
||||||
|
stateKegiatanDesa.create.form.kategoriKegiatanId?.trim() !== '' &&
|
||||||
|
file !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateKegiatanDesa.create.form = {
|
stateKegiatanDesa.create.form = {
|
||||||
judul: '',
|
judul: '',
|
||||||
@@ -273,8 +295,11 @@ function CreateKegiatanDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -27,6 +27,21 @@ function EditBentukKonservasiBerdasarkanAdat() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
!isHtmlEmpty(formData.judul) &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize data dari global state
|
// Initialize data dari global state
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!bentukKonservasiState.findById.data) {
|
if (!bentukKonservasiState.findById.data) {
|
||||||
@@ -137,8 +152,11 @@ function EditBentukKonservasiBerdasarkanAdat() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,21 @@ function EditFilosofiTriHitaKarana() {
|
|||||||
content: '',
|
content: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
!isHtmlEmpty(formData.judul) &&
|
||||||
|
!isHtmlEmpty(formData.content)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data dari global state kalau belum ada
|
// Load data dari global state kalau belum ada
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!filosofiTriHitaState.findById.data) {
|
if (!filosofiTriHitaState.findById.data) {
|
||||||
@@ -142,8 +157,11 @@ function EditFilosofiTriHitaKarana() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,21 @@ function EditNilaiKonservasiAdat() {
|
|||||||
const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
|
const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
|
||||||
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
|
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
!isHtmlEmpty(formData.judul) &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// load data awal
|
// load data awal
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!nilaiKonservasiState.findById.data) {
|
if (!nilaiKonservasiState.findById.data) {
|
||||||
@@ -136,8 +151,11 @@ function EditNilaiKonservasiAdat() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ function EditKeteranganBankSampahTerdekat() {
|
|||||||
lng: 0,
|
lng: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.alamat?.trim() !== '' &&
|
||||||
|
formData.namaTempatMaps?.trim() !== '' &&
|
||||||
|
markerPosition !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data ketika component mount
|
// Load data ketika component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKeterangan = async () => {
|
const loadKeterangan = async () => {
|
||||||
@@ -197,8 +207,11 @@ function EditKeteranganBankSampahTerdekat() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,16 @@ function CreateKeteranganBankSampahTerdekat() {
|
|||||||
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
|
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
keteranganState.create.form.name?.trim() !== '' &&
|
||||||
|
keteranganState.create.form.alamat?.trim() !== '' &&
|
||||||
|
keteranganState.create.form.namaTempatMaps?.trim() !== '' &&
|
||||||
|
markerPosition !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
keteranganState.create.form = {
|
keteranganState.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -135,8 +145,11 @@ function CreateKeteranganBankSampahTerdekat() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ function EditProgramKreatifDesa() {
|
|||||||
icon: '',
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.icon?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProgramKreatif = async () => {
|
const loadProgramKreatif = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -143,8 +151,11 @@ function EditProgramKreatifDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ function CreatePengelolaanSampahBankSampah() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.name?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.icon?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateCreate.create.form = {
|
stateCreate.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -91,8 +99,11 @@ function CreatePengelolaanSampahBankSampah() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -64,6 +64,23 @@ function EditProgramPenghijauan() {
|
|||||||
icon: '',
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi) &&
|
||||||
|
formData.icon?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data program penghijauan
|
// Load data program penghijauan
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProgram = async () => {
|
const loadProgram = async () => {
|
||||||
@@ -216,8 +233,11 @@ function EditProgramPenghijauan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ function CreateProgramPenghijauan() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.name?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.icon?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(stateCreate.create.form.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateCreate.create.form = {
|
stateCreate.create.form = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -128,8 +145,11 @@ function CreateProgramPenghijauan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,21 @@ function EditProgramKreatifDesa() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProgramKreatif = async () => {
|
const loadProgramKreatif = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -160,8 +175,11 @@ function EditProgramKreatifDesa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ function CreateKeunggulanProgram() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(stateCreate.create.form.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateCreate.create.form = {
|
stateCreate.create.form = {
|
||||||
judul: "",
|
judul: "",
|
||||||
@@ -97,8 +112,11 @@ function CreateKeunggulanProgram() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -42,6 +42,21 @@ function EditFasilitasYangDisediakan() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data pertama kali
|
// Load data pertama kali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) {
|
if (!editState.findById.data) {
|
||||||
@@ -76,11 +91,6 @@ function EditFasilitasYangDisediakan() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -180,8 +190,11 @@ function EditFasilitasYangDisediakan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,21 @@ function EditLokasiDanJadwal() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
|
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data sekali
|
// Load data sekali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) {
|
if (!editState.findById.data) {
|
||||||
@@ -73,11 +88,6 @@ function EditLokasiDanJadwal() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -178,8 +188,11 @@ function EditLokasiDanJadwal() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,21 @@ function EditTujuanProgram() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
|
const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// load data sekali
|
// load data sekali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) editState.findById.initialize();
|
if (!editState.findById.data) editState.findById.initialize();
|
||||||
@@ -71,11 +86,6 @@ function EditTujuanProgram() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -170,8 +180,11 @@ function EditTujuanProgram() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,6 +28,14 @@ export default function EditDataPendidikan() {
|
|||||||
jumlah: '',
|
jumlah: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.name?.trim() !== '' &&
|
||||||
|
formData.jumlah?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data saat mount
|
// Load data saat mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -127,8 +135,11 @@ export default function EditDataPendidikan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,14 @@ export default function CreateDataPendidikan() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateDPM.create.form.name?.trim() !== '' &&
|
||||||
|
stateDPM.create.form.jumlah?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateDPM.create.form = { name: '', jumlah: '' };
|
stateDPM.create.form = { name: '', jumlah: '' };
|
||||||
};
|
};
|
||||||
@@ -90,8 +98,11 @@ export default function CreateDataPendidikan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,11 @@ function EditJenjangPendidikan() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return formData.nama?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
// Load data sekali saat component mount
|
// Load data sekali saat component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -136,8 +141,11 @@ function EditJenjangPendidikan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ function CreateJenjangPendidikan() {
|
|||||||
const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan);
|
const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return stateJenjang.create.form.nama?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateJenjang.findMany.load();
|
stateJenjang.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -101,8 +106,11 @@ function CreateJenjangPendidikan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ export default function EditLembaga() {
|
|||||||
jenjangId: '',
|
jenjangId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
form.nama?.trim() !== '' &&
|
||||||
|
form.jenjangId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load jenjang pendidikan dan data lembaga
|
// Load jenjang pendidikan dan data lembaga
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
infoSekolahPaud.jenjangPendidikan.findMany.load();
|
infoSekolahPaud.jenjangPendidikan.findMany.load();
|
||||||
@@ -161,8 +169,11 @@ export default function EditLembaga() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ function CreateLembaga() {
|
|||||||
const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan);
|
const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateLembaga.create.form.nama?.trim() !== '' &&
|
||||||
|
stateLembaga.create.form.jenjangId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateLembaga.findMany.load();
|
stateLembaga.findMany.load();
|
||||||
infoSekolahPaud.jenjangPendidikan.findMany.load();
|
infoSekolahPaud.jenjangPendidikan.findMany.load();
|
||||||
@@ -116,8 +124,11 @@ function CreateLembaga() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,14 @@ function EditPengajar() {
|
|||||||
lembagaId: ''
|
lembagaId: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.nama?.trim() !== '' &&
|
||||||
|
formData.lembagaId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -157,8 +165,11 @@ function EditPengajar() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ function CreatePengajar() {
|
|||||||
const stateCreate = useProxy(infoSekolahPaud.pengajar);
|
const stateCreate = useProxy(infoSekolahPaud.pengajar);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.nama?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.lembagaId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateCreate.findMany.load();
|
stateCreate.findMany.load();
|
||||||
infoSekolahPaud.lembagaPendidikan.findMany.load();
|
infoSekolahPaud.lembagaPendidikan.findMany.load();
|
||||||
@@ -116,8 +124,11 @@ function CreatePengajar() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ function EditSiswa() {
|
|||||||
lembagaId: '',
|
lembagaId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.nama?.trim() !== '' &&
|
||||||
|
formData.lembagaId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data siswa
|
// Load data siswa
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSiswa = async () => {
|
const loadSiswa = async () => {
|
||||||
@@ -166,8 +174,11 @@ function EditSiswa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ function CreateSiswa() {
|
|||||||
const stateCreate = useProxy(infoSekolahPaud.siswa);
|
const stateCreate = useProxy(infoSekolahPaud.siswa);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
stateCreate.create.form.nama?.trim() !== '' &&
|
||||||
|
stateCreate.create.form.lembagaId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateCreate.findMany.load();
|
stateCreate.findMany.load();
|
||||||
infoSekolahPaud.lembagaPendidikan.findMany.load();
|
infoSekolahPaud.lembagaPendidikan.findMany.load();
|
||||||
@@ -115,8 +123,11 @@ function CreateSiswa() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ function EditJenisProgramYangDiselenggarakan() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [originalData, setOriginalData] = useState({ judul: '', content: '' });
|
const [originalData, setOriginalData] = useState({ judul: '', content: '' });
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.content)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data pertama kali
|
// Load data pertama kali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) {
|
if (!editState.findById.data) {
|
||||||
@@ -71,11 +86,6 @@ function EditJenisProgramYangDiselenggarakan() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -168,8 +178,11 @@ function EditJenisProgramYangDiselenggarakan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ function EditTempatKegiatan() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// load data pertama kali
|
// load data pertama kali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) {
|
if (!editState.findById.data) {
|
||||||
@@ -79,11 +94,6 @@ function EditTempatKegiatan() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -177,8 +187,11 @@ function EditTempatKegiatan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ function EditTujuanProgram() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data pertama kali
|
// Load data pertama kali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) {
|
if (!editState.findById.data) {
|
||||||
@@ -72,11 +87,6 @@ function EditTujuanProgram() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editState.findById.data) return;
|
if (!editState.findById.data) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@@ -163,8 +173,11 @@ function EditTujuanProgram() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,6 +38,22 @@ function EditPerpustakaanDigital() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | 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 (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi) &&
|
||||||
|
formData.kategoriId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load kategori & data awal
|
// Load kategori & data awal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
perpustakaanDigitalState.kategoriBuku.findManyAll.load();
|
perpustakaanDigitalState.kategoriBuku.findManyAll.load();
|
||||||
@@ -254,8 +270,11 @@ function EditPerpustakaanDigital() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -18,6 +18,23 @@ function CreateDataPerpustakaan() {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
createState.create.form.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(createState.create.form.deskripsi) &&
|
||||||
|
createState.create.form.kategoriId?.trim() !== '' &&
|
||||||
|
file !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
perpustakaanDigitalState.kategoriBuku.findManyAll.load();
|
perpustakaanDigitalState.kategoriBuku.findManyAll.load();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -196,8 +213,11 @@ function CreateDataPerpustakaan() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ function EditKategoriBuku() {
|
|||||||
const [formData, setFormData] = useState({ name: '' });
|
const [formData, setFormData] = useState({ name: '' });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return formData.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -120,8 +125,11 @@ function EditKategoriBuku() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ function CreateKategoriBuku() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return createState.create.form.name?.trim() !== '';
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -81,8 +86,11 @@ function CreateKategoriBuku() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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,26 @@ function EditPeminjam() {
|
|||||||
catatan: "",
|
catatan: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.nama?.trim() !== '' &&
|
||||||
|
formData.noTelp?.trim() !== '' &&
|
||||||
|
formData.alamat?.trim() !== '' &&
|
||||||
|
formData.bukuId?.trim() !== '' &&
|
||||||
|
formData.tanggalPinjam?.trim() !== '' &&
|
||||||
|
formData.status?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.catatan)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
|
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
|
||||||
})
|
})
|
||||||
@@ -296,8 +316,11 @@ function EditPeminjam() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -50,6 +50,21 @@ function EditTujuanProgram() {
|
|||||||
});
|
});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// load data once
|
// load data once
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) editState.findById.initialize();
|
if (!editState.findById.data) editState.findById.initialize();
|
||||||
@@ -85,11 +100,6 @@ function EditTujuanProgram() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -186,8 +196,11 @@ function EditTujuanProgram() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ function EditTujuanProgram() {
|
|||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
formData.judul?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(formData.deskripsi)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Load data pertama kali
|
// Load data pertama kali
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
if (!editState.findById.data) {
|
if (!editState.findById.data) {
|
||||||
@@ -72,11 +87,6 @@ function EditTujuanProgram() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.judul.trim()) {
|
|
||||||
toast.error('Judul wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (editState.findById.data) {
|
if (editState.findById.data) {
|
||||||
@@ -166,8 +176,11 @@ function EditTujuanProgram() {
|
|||||||
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, #eeeeee)`
|
||||||
|
: `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)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
|
||||||
import { authStore } from "@/store/authStore";
|
import { authStore } from "@/store/authStore";
|
||||||
|
import { useDarkMode } from "@/state/darkModeStore";
|
||||||
|
import { themeTokens, getActiveStateStyles } from "@/utils/themeTokens";
|
||||||
|
import { DarkModeToggle } from "@/components/admin/DarkModeToggle";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
AppShell,
|
AppShell,
|
||||||
@@ -33,13 +35,21 @@ import { useEffect, useState } from "react";
|
|||||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close'
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [opened, { toggle, close }] = useDisclosure();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||||
|
|
||||||
|
// Ensure component is mounted on client side
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
@@ -74,7 +84,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
if (currentPath === '/admin') {
|
if (currentPath === '/admin') {
|
||||||
const expectedPath = getRedirectPath(Number(data.user.roleId));
|
const expectedPath = getRedirectPath(Number(data.user.roleId));
|
||||||
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
||||||
@@ -112,11 +122,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading || !mounted) {
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<AppShellMain>
|
<AppShellMain>
|
||||||
<Center h="100vh">
|
<Center h="100vh" bg="#f6f9fc">
|
||||||
<Loader />
|
<Loader />
|
||||||
</Center>
|
</Center>
|
||||||
</AppShellMain>
|
</AppShellMain>
|
||||||
@@ -132,7 +142,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
setIsLoggingOut(true);
|
setIsLoggingOut(true);
|
||||||
|
|
||||||
const response = await fetch('/api/auth/logout', {
|
const response = await fetch('/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
@@ -158,10 +168,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ Handler untuk menutup mobile menu saat navigasi
|
|
||||||
const handleNavClick = (path: string) => {
|
const handleNavClick = (path: string) => {
|
||||||
router.push(path);
|
router.push(path);
|
||||||
close(); // Tutup mobile menu
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -178,11 +187,16 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
|
{/*
|
||||||
|
HEADER / TOPBAR
|
||||||
|
Spec: Background gradient, border bawah wajib
|
||||||
|
*/}
|
||||||
<AppShellHeader
|
<AppShellHeader
|
||||||
style={{
|
style={{
|
||||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
background: mounted ? tokens.colors.bg.header : 'linear-gradient(90deg, #ffffff, #f9fbff)',
|
||||||
borderBottom: `1px solid ${colors["blue-button"]}20`,
|
borderBottom: `1px solid ${mounted ? tokens.colors.border.soft : '#e9ecef'}`,
|
||||||
padding: '0 16px',
|
padding: '0 16px',
|
||||||
|
transition: 'background 0.3s ease, border-color 0.3s ease',
|
||||||
}}
|
}}
|
||||||
px={{ base: 'sm', sm: 'md' }}
|
px={{ base: 'sm', sm: 'md' }}
|
||||||
py={{ base: 'xs', sm: 'sm' }}
|
py={{ base: 'xs', sm: 'sm' }}
|
||||||
@@ -198,30 +212,49 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ minWidth: '32px', height: 'auto' }}
|
style={{ minWidth: '32px', height: 'auto' }}
|
||||||
/>
|
/>
|
||||||
<Text fw={700} c={colors["blue-button"]} fz={{ base: 'md', sm: 'xl' }}>
|
<Text fw={700} c={mounted ? tokens.colors.text.brand : '#0A4E78'} fz={{ base: 'md', sm: 'xl' }}>
|
||||||
Admin Darmasaba
|
Admin Darmasaba
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
|
{/* Dark Mode Toggle */}
|
||||||
|
<DarkModeToggle variant="light" size="lg" showTooltip tooltipPosition="bottom" />
|
||||||
|
|
||||||
{!desktopOpened && (
|
{!desktopOpened && (
|
||||||
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
<Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||||
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={mounted ? tokens.colors.primary : '#3B82F6'}>
|
||||||
<IconChevronRight />
|
<IconChevronRight />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="md" color={colors["blue-button"]} mr="xs" />
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="md" color={mounted ? tokens.colors.text.brand : '#0A4E78'} mr="xs" />
|
||||||
|
|
||||||
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
<Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||||
<ActionIcon onClick={() => router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}>
|
<ActionIcon
|
||||||
|
onClick={() => router.push("/darmasaba")}
|
||||||
|
color={mounted ? tokens.colors.primary : '#3B82F6'}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={mounted ? tokens.colors.gradient : { from: '#3B82F6', to: '#60A5FA' }}
|
||||||
|
>
|
||||||
<Image src="/assets/images/darmasaba-icon.png" alt="Logo Darmasaba" w={20} h={20} radius="md" loading="lazy" style={{ minWidth: '20px', height: 'auto' }} />
|
<Image src="/assets/images/darmasaba-icon.png" alt="Logo Darmasaba" w={20} h={20} radius="md" loading="lazy" style={{ minWidth: '20px', height: 'auto' }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="Keluar" position="bottom" withArrow>
|
<Tooltip label="Keluar" position="bottom" withArrow>
|
||||||
<ActionIcon onClick={handleLogout} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }} loading={isLoggingOut} disabled={isLoggingOut}>
|
<ActionIcon
|
||||||
|
onClick={handleLogout}
|
||||||
|
color={mounted ? tokens.colors.primary : '#3B82F6'}
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={mounted ? tokens.colors.gradient : { from: '#3B82F6', to: '#60A5FA' }}
|
||||||
|
loading={isLoggingOut}
|
||||||
|
disabled={isLoggingOut}
|
||||||
|
>
|
||||||
<IconLogout2 size={22} />
|
<IconLogout2 size={22} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -229,47 +262,105 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</Group>
|
</Group>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
|
|
||||||
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
{/*
|
||||||
|
SIDEBAR / NAVBAR
|
||||||
|
Spec: Background --bg-app, active state dengan accent bar
|
||||||
|
*/}
|
||||||
|
<AppShellNavbar
|
||||||
|
component={ScrollArea}
|
||||||
|
style={{
|
||||||
|
background: mounted ? tokens.colors.bg.app : '#ffffff',
|
||||||
|
borderRight: `1px solid ${mounted ? tokens.colors.border.soft : '#e9ecef'}`,
|
||||||
|
transition: 'background 0.3s ease, border-color 0.3s ease',
|
||||||
|
}}
|
||||||
|
p={{ base: 'xs', sm: 'sm' }}
|
||||||
|
>
|
||||||
<AppShell.Section p="sm">
|
<AppShell.Section p="sm">
|
||||||
{currentNav.map((v, k) => {
|
{currentNav.map((v, k) => {
|
||||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={k}
|
key={k}
|
||||||
defaultOpened={isParentActive}
|
defaultOpened={isParentActive}
|
||||||
c={isParentActive ? colors["blue-button"] : "gray"}
|
c={mounted && isParentActive ? tokens.colors.primary : mounted && isDark ? '#E5E7EB' : tokens.colors.text.secondary}
|
||||||
label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>}
|
label={
|
||||||
style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }}
|
<Text
|
||||||
styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }}
|
fw={isParentActive ? 600 : 400}
|
||||||
variant="light"
|
fz="sm"
|
||||||
|
style={{
|
||||||
|
color: mounted && isDark ? '#E5E7EB' : 'inherit',
|
||||||
|
transition: 'color 150ms ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{v.name}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderRadius: rem(10),
|
||||||
|
marginBottom: rem(4),
|
||||||
|
transition: "background 150ms ease",
|
||||||
|
...(mounted && isParentActive && !isDark && {
|
||||||
|
borderLeft: `3px solid ${tokens.colors.primary}`,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: mounted && isDark ? '#1E293B' : tokens.colors.bg.hover,
|
||||||
|
},
|
||||||
|
...(mounted && isParentActive && isDark && {
|
||||||
|
backgroundColor: 'rgba(59,130,246,0.25)',
|
||||||
|
borderLeft: `3px solid ${tokens.colors.primary}`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
active={isParentActive}
|
active={isParentActive}
|
||||||
>
|
>
|
||||||
{v.children.map((child, key) => {
|
{v.children.map((child, key) => {
|
||||||
const isChildActive = segments.includes(_.lowerCase(child.name));
|
const isChildActive = segments.includes(_.lowerCase(child.name));
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={key}
|
key={key}
|
||||||
// ✅ PERBAIKAN: Gunakan onClick untuk handle navigasi dan close menu
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleNavClick(child.path);
|
handleNavClick(child.path);
|
||||||
}}
|
}}
|
||||||
href={child.path}
|
href={child.path}
|
||||||
c={isChildActive ? colors["blue-button"] : "gray"}
|
c={mounted && isChildActive ? tokens.colors.primary : mounted && isDark ? '#E5E7EB' : tokens.colors.text.secondary}
|
||||||
label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>}
|
label={
|
||||||
styles={{
|
<Text
|
||||||
root: {
|
fw={isChildActive ? 600 : 400}
|
||||||
borderRadius: rem(8),
|
fz="sm"
|
||||||
marginBottom: rem(2),
|
style={{
|
||||||
transition: 'background 150ms ease',
|
color: mounted && isDark ? '#E5E7EB' : 'inherit',
|
||||||
padding: '6px 12px',
|
transition: 'color 150ms ease',
|
||||||
'&:hover': {
|
}}
|
||||||
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)'
|
>
|
||||||
},
|
{child.name}
|
||||||
...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' })
|
</Text>
|
||||||
}
|
}
|
||||||
}}
|
styles={{
|
||||||
active={isChildActive}
|
root: {
|
||||||
|
borderRadius: rem(8),
|
||||||
|
marginBottom: rem(2),
|
||||||
|
transition: 'background 150ms ease',
|
||||||
|
padding: '6px 12px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: mounted && isDark ? 'rgba(255, 255, 255, 0.05)' : tokens.colors.bg.hover,
|
||||||
|
},
|
||||||
|
...(mounted && isChildActive && isDark && {
|
||||||
|
backgroundColor: 'rgba(59,130,246,0.15)',
|
||||||
|
borderLeft: `2px solid ${tokens.colors.primary}`,
|
||||||
|
}),
|
||||||
|
...(mounted && isChildActive && !isDark && {
|
||||||
|
backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||||
|
borderLeft: `2px solid ${tokens.colors.primary}`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
active={isChildActive}
|
||||||
|
variant="subtle"
|
||||||
component={Link}
|
component={Link}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -282,7 +373,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<AppShell.Section py="md">
|
<AppShell.Section py="md">
|
||||||
<Group justify="end" pr="sm">
|
<Group justify="end" pr="sm">
|
||||||
<Tooltip label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"} position="top" withArrow>
|
<Tooltip label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"} position="top" withArrow>
|
||||||
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={colors["blue-button"]}>
|
<ActionIcon variant="light" radius="xl" size="lg" onClick={toggleDesktop} color={mounted ? tokens.colors.primary : '#3B82F6'}>
|
||||||
<IconChevronLeft />
|
<IconChevronLeft />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -290,7 +381,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</AppShell.Section>
|
</AppShell.Section>
|
||||||
</AppShellNavbar>
|
</AppShellNavbar>
|
||||||
|
|
||||||
<AppShellMain style={{ background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)", minHeight: "100vh" }}>
|
{/*
|
||||||
|
MAIN CONTENT
|
||||||
|
Spec: Background --bg-base
|
||||||
|
*/}
|
||||||
|
<AppShellMain
|
||||||
|
style={{
|
||||||
|
background: mounted ? tokens.colors.bg.base : '#f6f9fc',
|
||||||
|
minHeight: "100vh",
|
||||||
|
transition: 'background 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AppShellMain>
|
</AppShellMain>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|||||||
@@ -21,8 +21,13 @@ export default async function findUnique(
|
|||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await prisma.potensiDesa.findUnique({
|
// ✅ Filter by isActive and deletedAt
|
||||||
where: { id },
|
const data = await prisma.potensiDesa.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
kategori: true
|
kategori: true
|
||||||
@@ -48,5 +53,5 @@ export default async function findUnique(
|
|||||||
message: "Gagal mengambil potensi desa: " + (error instanceof Error ? error.message : 'Unknown error'),
|
message: "Gagal mengambil potensi desa: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,50 @@ import prisma from "@/lib/prisma";
|
|||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function kategoriPotensiDelete(context: Context) {
|
export default async function kategoriPotensiDelete(context: Context) {
|
||||||
const id = context.params.id as string;
|
try {
|
||||||
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
await prisma.kategoriPotensi.delete({
|
if (!id) {
|
||||||
where: { id },
|
return Response.json({
|
||||||
});
|
success: false,
|
||||||
|
message: "ID tidak boleh kosong",
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
// ✅ Cek apakah kategori masih digunakan oleh potensi desa
|
||||||
status: 200,
|
const existingPotensi = await prisma.potensiDesa.findFirst({
|
||||||
success: true,
|
where: {
|
||||||
message: "Sukses Menghapus kategori potensi",
|
kategoriId: id,
|
||||||
};
|
isActive: true,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingPotensi) {
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Kategori masih digunakan oleh potensi desa. Tidak dapat dihapus.",
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
await prisma.kategoriPotensi.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Kategori potensi berhasil dihapus",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Delete kategori error:", error);
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { requireAuth } from "@/lib/api-auth";
|
||||||
|
|
||||||
|
export default async function sejarahDesaFindFirst(request: Request) {
|
||||||
|
// ✅ Authentication check
|
||||||
|
const headers = new Headers(request.url);
|
||||||
|
const authResult = await requireAuth({ headers });
|
||||||
|
if (!authResult.authenticated) {
|
||||||
|
return authResult.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the first active record
|
||||||
|
const data = await prisma.sejarahDesa.findFirst({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' } // Get the oldest one first
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Data tidak ditemukan",
|
||||||
|
}, {status: 404})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
}, {status: 200})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal mengambil data sejarah desa:", error)
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi kesalahan saat mengambil data",
|
||||||
|
}, {status: 500})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
import sejarahDesaFindById from "./find-by-id";
|
import sejarahDesaFindById from "./find-by-id";
|
||||||
import sejarahDesaUpdate from "./update";
|
import sejarahDesaUpdate from "./update";
|
||||||
|
import sejarahDesaFindFirst from "./find-first";
|
||||||
|
|
||||||
const SejarahDesa = new Elysia({
|
const SejarahDesa = new Elysia({
|
||||||
prefix: "/sejarah",
|
prefix: "/sejarah",
|
||||||
tags: ["Desa/Profile"],
|
tags: ["Desa/Profile"],
|
||||||
})
|
})
|
||||||
|
.get("/first", async (context) => {
|
||||||
|
const response = await sejarahDesaFindFirst(new Request(context.request));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
.get("/:id", async (context) => {
|
.get("/:id", async (context) => {
|
||||||
const response = await sejarahDesaFindById(new Request(context.request));
|
const response = await sejarahDesaFindById(new Request(context.request));
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { requireAuth } from "@/lib/api-auth";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function sejarahDesaUpdate(context: Context) {
|
export default async function sejarahDesaUpdate(context: Context) {
|
||||||
|
// ✅ Authentication check
|
||||||
|
const authResult = await requireAuth(context);
|
||||||
|
if (!authResult.authenticated) {
|
||||||
|
return authResult.response;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = context.params?.id as string;
|
const id = context.params?.id as string;
|
||||||
const body = await context.body as {
|
const body = await context.body as {
|
||||||
|
|||||||
@@ -12,6 +12,25 @@ function Page() {
|
|||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const ideInovatif = useProxy(ajukanIdeInovatifState);
|
const ideInovatif = useProxy(ajukanIdeInovatifState);
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
ideInovatif.create.form.name?.trim() !== '' &&
|
||||||
|
ideInovatif.create.form.alamat?.trim() !== '' &&
|
||||||
|
ideInovatif.create.form.namaIde?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(ideInovatif.create.form.deskripsi) &&
|
||||||
|
ideInovatif.create.form.masalah?.trim() !== '' &&
|
||||||
|
ideInovatif.create.form.benefit?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
ideInovatif.create.form = {
|
ideInovatif.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -168,7 +187,11 @@ function Page() {
|
|||||||
ideInovatif.create.form.benefit = val.target.value;
|
ideInovatif.create.form.benefit = val.target.value;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
<Button
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
>
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ function AdministrasiOnline() {
|
|||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const state = useProxy(layananonlineDesa);
|
const state = useProxy(layananonlineDesa);
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
state.administrasiOnline.create.form.name?.trim() !== '' &&
|
||||||
|
state.administrasiOnline.create.form.alamat?.trim() !== '' &&
|
||||||
|
state.administrasiOnline.create.form.nomorTelepon?.trim() !== '' &&
|
||||||
|
state.administrasiOnline.create.form.jenisLayananId?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ✅ Panggil load data jenis layanan dari backend
|
// ✅ Panggil load data jenis layanan dari backend
|
||||||
if (!state.jenisLayanan.findMany.data) {
|
if (!state.jenisLayanan.findMany.data) {
|
||||||
@@ -104,7 +114,11 @@ function AdministrasiOnline() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
<Button
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
>
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ function PengaduanMasyarakat() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | 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 (
|
||||||
|
state.pengaduanMasyarakat.create.form.name?.trim() !== '' &&
|
||||||
|
state.pengaduanMasyarakat.create.form.email?.trim() !== '' &&
|
||||||
|
state.pengaduanMasyarakat.create.form.nomorTelepon?.trim() !== '' &&
|
||||||
|
state.pengaduanMasyarakat.create.form.nik?.trim() !== '' &&
|
||||||
|
state.pengaduanMasyarakat.create.form.judulPengaduan?.trim() !== '' &&
|
||||||
|
state.pengaduanMasyarakat.create.form.lokasiKejadian?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(state.pengaduanMasyarakat.create.form.deskripsiPengaduan) &&
|
||||||
|
state.pengaduanMasyarakat.create.form.jenisPengaduanId?.trim() !== '' &&
|
||||||
|
file !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ✅ Panggil load data jenis layanan dari backend
|
// ✅ Panggil load data jenis layanan dari backend
|
||||||
if (!state.jenisPengaduan.findMany.data) {
|
if (!state.jenisPengaduan.findMany.data) {
|
||||||
@@ -207,7 +229,11 @@ function PengaduanMasyarakat() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
<Button
|
||||||
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!isFormValid()}
|
||||||
|
>
|
||||||
Simpan
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -37,6 +37,24 @@ function Page() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
beasiswaDesa.create.form.namaLengkap?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.nis?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.kelas?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.jenisKelamin?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.alamatDomisili?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.tempatLahir?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.tanggalLahir?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.namaOrtu?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.nik?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.penghasilan?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.noHp?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const { data, page, totalPages, loading, load } = ungggulanDesa.findMany;
|
const { data, page, totalPages, loading, load } = ungggulanDesa.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
@@ -238,7 +256,7 @@ function Page() {
|
|||||||
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
|
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
|
||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
|
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
|
||||||
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
|
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit} disabled={!isFormValid()}>Kirim</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -46,6 +46,24 @@ export default function BeasiswaPage() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
beasiswaDesa.create.form.namaLengkap?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.nis?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.kelas?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.jenisKelamin?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.alamatDomisili?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.tempatLahir?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.tanggalLahir?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.namaOrtu?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.nik?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.penghasilan?.trim() !== '' &&
|
||||||
|
beasiswaDesa.create.form.noHp?.trim() !== ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await beasiswaDesa.create.create();
|
await beasiswaDesa.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
@@ -391,6 +409,7 @@ export default function BeasiswaPage() {
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
disabled={!isFormValid()}
|
||||||
style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }}
|
style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }}
|
||||||
>
|
>
|
||||||
Kirim
|
Kirim
|
||||||
|
|||||||
@@ -42,6 +42,24 @@ export default function ModalPeminjaman({
|
|||||||
|
|
||||||
const BATAS_HARI_PINJAM = 4;
|
const BATAS_HARI_PINJAM = 4;
|
||||||
|
|
||||||
|
// Helper function to check if HTML content is empty
|
||||||
|
const isHtmlEmpty = (html: string) => {
|
||||||
|
// Remove all HTML tags and check if there's any text content
|
||||||
|
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
||||||
|
return textContent === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if form is valid
|
||||||
|
const isFormValid = () => {
|
||||||
|
return (
|
||||||
|
snap.create.form.nama?.trim() !== '' &&
|
||||||
|
snap.create.form.noTelp?.trim() !== '' &&
|
||||||
|
snap.create.form.alamat?.trim() !== '' &&
|
||||||
|
snap.create.form.tanggalPinjam?.trim() !== '' &&
|
||||||
|
!isHtmlEmpty(snap.create.form.catatan)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Reset form setiap modal dibuka
|
// Reset form setiap modal dibuka
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened && buku) {
|
if (opened && buku) {
|
||||||
@@ -222,13 +240,13 @@ export default function ModalPeminjaman({
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={snap.create.loading}
|
loading={snap.create.loading}
|
||||||
disabled={
|
disabled={!isFormValid() || snap.create.loading}
|
||||||
!snap.create.form.nama || !snap.create.form.tanggalPinjam
|
|
||||||
}
|
|
||||||
rightSection={<IconArrowRight size={16} />}
|
rightSection={<IconArrowRight size={16} />}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: !isFormValid() || snap.create.loading
|
||||||
|
? `linear-gradient(135deg, #cccccc, #eeeeee)`
|
||||||
|
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { NavbarSubMenu } from "./NavbarSubMenu"
|
|||||||
import { authStore } from "@/store/authStore";
|
import { authStore } from "@/store/authStore";
|
||||||
|
|
||||||
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
|
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
|
||||||
const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
|
const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
|
||||||
|
|
||||||
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||||
const { item, isSearch } = useSnapshot(stateNav)
|
const { item, isSearch } = useSnapshot(stateNav)
|
||||||
@@ -46,11 +46,11 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{listNavbar.map((item, k) => (
|
{listNavbar.map((item, k) => (
|
||||||
<MenuItemCom
|
<MenuItemCom
|
||||||
key={k}
|
key={k}
|
||||||
item={item}
|
item={item}
|
||||||
isActive={item.href && pathname.startsWith(item.href) ||
|
isActive={item.href && pathname.startsWith(item.href) ||
|
||||||
(item.children?.some(child => child.href && pathname.startsWith(child.href)))}
|
(item.children?.some(child => child.href && pathname.startsWith(child.href)))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
|
|||||||
<Tooltip label="Kembali ke Admin" position="bottom" withArrow>
|
<Tooltip label="Kembali ke Admin" position="bottom" withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
next.push("/admin/landing-page/profil/program-inovasi")
|
next.push("/admin/landing-page/profile/program-inovasi")
|
||||||
}}
|
}}
|
||||||
color={colors["blue-button"]}
|
color={colors["blue-button"]}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
useMantineColorScheme
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
@@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr
|
|||||||
|
|
||||||
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const isDark = colorScheme === "dark";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div whileHover={{ scale: 1.03 }}>
|
<motion.div whileHover={{ scale: 1.03 }}>
|
||||||
@@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="cursor-pointer transition-all"
|
className="cursor-pointer transition-all"
|
||||||
bg={isDark ? "dark.6" : "white"}
|
bg="white"
|
||||||
>
|
>
|
||||||
<Center h={160}>
|
<Center h={160}>
|
||||||
{data.image?.link ? (
|
{data.image?.link ? (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import { Box, Space, Stack } from "@mantine/core";
|
import { Box, Space, Stack } from "@mantine/core";
|
||||||
|
|
||||||
@@ -5,21 +7,20 @@ import { Navbar } from "@/app/darmasaba/_com/Navbar";
|
|||||||
import Footer from "./_com/Footer";
|
import Footer from "./_com/Footer";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} bg={colors.grey[1]}>
|
<Stack gap={0} bg={colors.grey[1]}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Space h={{
|
<Space h={{
|
||||||
base: "3.9rem",
|
base: "3.9rem",
|
||||||
md: "2.5rem"
|
md: "2.5rem"
|
||||||
}} />
|
}} />
|
||||||
<Box style={{
|
<Box style={{
|
||||||
overflow: "scroll"
|
overflow: "scroll"
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -98,16 +98,16 @@ export default function RootLayout({
|
|||||||
<html lang="id" {...mantineHtmlProps}>
|
<html lang="id" {...mantineHtmlProps}>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<ColorSchemeScript />
|
<ColorSchemeScript defaultColorScheme="light" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||||
{children}
|
{children}
|
||||||
<LoadDataFirstClient />
|
<LoadDataFirstClient />
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="bottom-center"
|
position="bottom-center"
|
||||||
hideProgressBar
|
hideProgressBar
|
||||||
style={{ zIndex: 9999 }}
|
style={{ zIndex: 9999 }}
|
||||||
/>
|
/>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
119
src/components/admin/AdminThemeProvider.tsx
Normal file
119
src/components/admin/AdminThemeProvider.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
import { MantineProvider, createTheme } from '@mantine/core';
|
||||||
|
import '@mantine/core/styles.css';
|
||||||
|
import '@/styles/dark-mode-table.css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin Theme Provider
|
||||||
|
*
|
||||||
|
* Wrapper untuk MantineProvider dengan custom theme
|
||||||
|
* Mendukung dark mode otomatis
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { AdminThemeProvider } from '@/components/admin/AdminThemeProvider';
|
||||||
|
*
|
||||||
|
* <AdminThemeProvider>
|
||||||
|
* <YourComponent />
|
||||||
|
* </AdminThemeProvider>
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface AdminThemeProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
forceTheme?: 'light' | 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminThemeProvider({ children, forceTheme }: AdminThemeProviderProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
|
||||||
|
// Use forced theme if provided, otherwise use store
|
||||||
|
const useDark = forceTheme ? forceTheme === 'dark' : isDark;
|
||||||
|
const tokens = themeTokens(useDark);
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
colors: {
|
||||||
|
primary: [
|
||||||
|
tokens.colors.primaryLight,
|
||||||
|
tokens.colors.primaryLight,
|
||||||
|
tokens.colors.primary,
|
||||||
|
tokens.colors.primary,
|
||||||
|
tokens.colors.primary,
|
||||||
|
tokens.colors.primary,
|
||||||
|
tokens.colors.primaryDark,
|
||||||
|
tokens.colors.primaryDark,
|
||||||
|
tokens.colors.primaryDark,
|
||||||
|
tokens.colors.primaryDark,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
primaryColor: 'primary',
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
|
fontFamilyMonospace: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
|
||||||
|
|
||||||
|
// Override default colors based on mode
|
||||||
|
white: tokens.colors.text.inverse,
|
||||||
|
black: tokens.colors.text.primary,
|
||||||
|
|
||||||
|
// CSS variables for table hover
|
||||||
|
activeClassName: useDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)',
|
||||||
|
|
||||||
|
// Component defaults
|
||||||
|
components: {
|
||||||
|
Paper: {
|
||||||
|
defaultProps: {
|
||||||
|
bg: tokens.colors.bg.card,
|
||||||
|
radius: 'md',
|
||||||
|
shadow: 'sm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: 'md',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TextInput: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: 'md',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: 'md',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Modal: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: 'lg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Table: {
|
||||||
|
defaultProps: {
|
||||||
|
highlightOnHover: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider
|
||||||
|
theme={theme}
|
||||||
|
forceColorScheme={useDark ? 'dark' : 'light'}
|
||||||
|
defaultColorScheme={useDark ? 'dark' : 'light'}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: tokens.colors.bg.main,
|
||||||
|
color: tokens.colors.text.primary,
|
||||||
|
minHeight: '100vh',
|
||||||
|
transition: 'background-color 0.3s ease, color 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminThemeProvider;
|
||||||
78
src/components/admin/DarkModeToggle.tsx
Normal file
78
src/components/admin/DarkModeToggle.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
import { ActionIcon, Tooltip, Transition } from '@mantine/core';
|
||||||
|
import { IconMoon, IconSun } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dark Mode Toggle Button
|
||||||
|
*
|
||||||
|
* Component untuk toggle dark/light mode
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { DarkModeToggle } from '@/components/admin/DarkModeToggle';
|
||||||
|
*
|
||||||
|
* <DarkModeToggle />
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DarkModeToggleProps {
|
||||||
|
variant?: 'light' | 'filled' | 'outline' | 'subtle';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
color?: string;
|
||||||
|
showTooltip?: boolean;
|
||||||
|
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DarkModeToggle({
|
||||||
|
variant = 'light',
|
||||||
|
size = 'lg',
|
||||||
|
color,
|
||||||
|
showTooltip = true,
|
||||||
|
tooltipPosition = 'bottom',
|
||||||
|
}: DarkModeToggleProps) {
|
||||||
|
const { isDark, toggle } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const iconColor = color || tokens.colors.primary;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={isDark ? 'Mode Terang' : 'Mode Gelap'}
|
||||||
|
position={tooltipPosition}
|
||||||
|
withArrow
|
||||||
|
disabled={!showTooltip}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
radius="xl"
|
||||||
|
onClick={toggle}
|
||||||
|
color={iconColor}
|
||||||
|
style={{
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
transform: 'scale(1)',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Icon Sun untuk Light Mode */}
|
||||||
|
<Transition mounted={!isDark} transition="scale" duration={200}>
|
||||||
|
{(style) => (
|
||||||
|
<IconSun style={style} size={20} stroke={1.5} />
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
{/* Icon Moon untuk Dark Mode */}
|
||||||
|
<Transition mounted={isDark} transition="scale" duration={200}>
|
||||||
|
{(style) => (
|
||||||
|
<IconMoon style={style} size={20} stroke={1.5} />
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DarkModeToggle;
|
||||||
546
src/components/admin/README_UNIFIED_STYLING.md
Normal file
546
src/components/admin/README_UNIFIED_STYLING.md
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
# 🎨 Unified Styling System - Admin Dashboard
|
||||||
|
|
||||||
|
Sistem styling terpusat untuk admin dashboard Darmasaba dengan dukungan **dark mode**.
|
||||||
|
|
||||||
|
**Berdasarkan spesifikasi:** `darkMode.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Daftar Isi
|
||||||
|
|
||||||
|
- [Konsep Utama](#konsep-utama)
|
||||||
|
- [Dark Mode Palette](#dark-mode-palette)
|
||||||
|
- [Struktur File](#struktur-file)
|
||||||
|
- [Cara Menggunakan](#cara-menggunakan)
|
||||||
|
- [Mengedit Style](#mengedit-style)
|
||||||
|
- [Dark Mode Toggle](#dark-mode-toggle)
|
||||||
|
- [Contoh Penggunaan](#contoh-penggunaan)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Konsep Utama
|
||||||
|
|
||||||
|
**Satu File Edit = Semua Halaman Terupdate**
|
||||||
|
|
||||||
|
Sebelumnya:
|
||||||
|
- ❌ Style tersebar di 493 file `.tsx`
|
||||||
|
- ❌ Hardcode warna di setiap komponen
|
||||||
|
- ❌ Tidak ada konsistensi
|
||||||
|
- ❌ Sulit maintain
|
||||||
|
|
||||||
|
Sekarang:
|
||||||
|
- ✅ Edit di **1 file** = semua halaman update
|
||||||
|
- ✅ Component reusable
|
||||||
|
- ✅ Konsisten di seluruh aplikasi
|
||||||
|
- ✅ Dark mode otomatis sesuai spesifikasi `darkMode.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌙 Dark Mode Palette
|
||||||
|
|
||||||
|
### Background Layers (Dark Mode)
|
||||||
|
| Layer | Token | Warna | Fungsi |
|
||||||
|
|------|------|------|------|
|
||||||
|
| Base | `bg.base` | `#0B1220` | Background utama aplikasi |
|
||||||
|
| App | `bg.app` | `#0F172A` | Area sidebar |
|
||||||
|
| Card | `bg.card` | `#162235` | Card / container |
|
||||||
|
| Surface | `bg.surface` | `#1E2A3D` | Table header, tab, input |
|
||||||
|
|
||||||
|
### Text Colors (Dark Mode)
|
||||||
|
| Jenis | Token | Warna |
|
||||||
|
|-----|------|------|
|
||||||
|
| Primary | `text.primary` | `#E5E7EB` |
|
||||||
|
| Secondary | `text.secondary` | `#9CA3AF` |
|
||||||
|
| Muted | `text.muted` | `#6B7280` |
|
||||||
|
|
||||||
|
### Accent & Actions (Dark Mode)
|
||||||
|
| Fungsi | Warna |
|
||||||
|
|------|------|
|
||||||
|
| Primary Action | `#3B82F6` |
|
||||||
|
| Hover | `#2563EB` |
|
||||||
|
| Active | `#1D4ED8` |
|
||||||
|
| Link | `#60A5FA` |
|
||||||
|
|
||||||
|
### Borders (Dark Mode)
|
||||||
|
| Token | Warna |
|
||||||
|
|-----|------|
|
||||||
|
| `border.default` | `#2A3A52` |
|
||||||
|
| `border.soft` | `#22314A` |
|
||||||
|
|
||||||
|
> **Catatan:** Light mode menggunakan palette original yang lebih terang
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Struktur File
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── utils/
|
||||||
|
│ └── themeTokens.ts # 📦 PUSAT SEMUA STYLE (edit di sini!)
|
||||||
|
├── state/
|
||||||
|
│ └── darkModeStore.ts # 🌙 State management dark mode
|
||||||
|
├── components/admin/
|
||||||
|
│ ├── DarkModeToggle.tsx # 🌓 Toggle button
|
||||||
|
│ ├── AdminThemeProvider.tsx # 🎨 Theme provider wrapper
|
||||||
|
│ ├── UnifiedTypography.tsx # 📝 Text components (Title, Text)
|
||||||
|
│ ├── UnifiedSurface.tsx # 📦 Card, Paper components
|
||||||
|
│ └── README_UNIFIED_STYLING.md # 📖 Dokumentasi ini
|
||||||
|
├── app/admin/
|
||||||
|
│ ├── layout.tsx # ✅ Sudah diupdate dengan dark mode
|
||||||
|
│ └── (dashboard)/
|
||||||
|
│ └── _com/
|
||||||
|
│ ├── header.tsx # ✅ Sudah diupdate
|
||||||
|
│ ├── judulList.tsx # ✅ Sudah diupdate
|
||||||
|
│ └── judulListTab.tsx # ✅ Sudah diupdate
|
||||||
|
└── darkMode.md # 📐 Spesifikasi lengkap dark mode
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cara Menggunakan
|
||||||
|
|
||||||
|
### 1. **Untuk Developer: Edit Style Global**
|
||||||
|
|
||||||
|
Edit file: `src/utils/themeTokens.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const themeTokens = (isDark: boolean = false): ThemeTokens => {
|
||||||
|
const darkColors = {
|
||||||
|
bgBase: '#0B1220', // ← Edit warna dark mode di sini
|
||||||
|
bgCard: '#162235',
|
||||||
|
textPrimary: '#E5E7EB',
|
||||||
|
primaryAction: '#3B82F6',
|
||||||
|
// ... dan lainnya
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
colors: {
|
||||||
|
primary: current.primaryAction,
|
||||||
|
bg: {
|
||||||
|
base: current.bgBase,
|
||||||
|
card: current.bgCard,
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Menggunakan Components di Halaman**
|
||||||
|
|
||||||
|
#### A. Typography Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography';
|
||||||
|
|
||||||
|
// Heading - otomatis dark mode
|
||||||
|
<UnifiedTitle order={1}>Judul Halaman</UnifiedTitle>
|
||||||
|
<UnifiedTitle order={2}>Sub Judul</UnifiedTitle>
|
||||||
|
<UnifiedTitle order={3}>Section Title</UnifiedTitle>
|
||||||
|
<UnifiedTitle order={4}>Card Title</UnifiedTitle>
|
||||||
|
|
||||||
|
// Text dengan color semantic
|
||||||
|
<UnifiedText size="body" color="primary">Teks primary</UnifiedText>
|
||||||
|
<UnifiedText size="body" color="secondary">Teks secondary</UnifiedText>
|
||||||
|
<UnifiedText size="body" color="muted">Teks muted</UnifiedText>
|
||||||
|
<UnifiedText size="body" color="link">Link text</UnifiedText>
|
||||||
|
<UnifiedText size="body" color="brand">Brand color</UnifiedText>
|
||||||
|
|
||||||
|
// Dengan weight
|
||||||
|
<UnifiedText weight="bold">Teks bold</UnifiedText>
|
||||||
|
<UnifiedText weight="medium">Teks medium</UnifiedText>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Surface Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface';
|
||||||
|
|
||||||
|
// Card sederhana - border dan warna otomatis dark mode
|
||||||
|
<UnifiedCard>
|
||||||
|
<p>Isi card</p>
|
||||||
|
</UnifiedCard>
|
||||||
|
|
||||||
|
// Card dengan sections
|
||||||
|
<UnifiedCard>
|
||||||
|
<UnifiedCard.Header>
|
||||||
|
<UnifiedTitle order={4}>Header</UnifiedTitle>
|
||||||
|
</UnifiedCard.Header>
|
||||||
|
|
||||||
|
<UnifiedCard.Body>
|
||||||
|
<p>Body content</p>
|
||||||
|
</UnifiedCard.Body>
|
||||||
|
|
||||||
|
<UnifiedCard.Footer>
|
||||||
|
<Button>Action</Button>
|
||||||
|
</UnifiedCard.Footer>
|
||||||
|
</UnifiedCard>
|
||||||
|
|
||||||
|
// Divider dengan variant
|
||||||
|
<UnifiedDivider variant="soft" /> {/* Default */}
|
||||||
|
<UnifiedDivider variant="default" />
|
||||||
|
<UnifiedDivider variant="strong" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C. Page Header Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { UnifiedPageHeader } from '@/components/admin/UnifiedTypography';
|
||||||
|
|
||||||
|
<UnifiedPageHeader
|
||||||
|
title="Daftar Berita"
|
||||||
|
subtitle="Kelola semua berita di sini"
|
||||||
|
action={
|
||||||
|
<Button onClick={handleCreate}>
|
||||||
|
<IconPlus /> Tambah Baru
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Menggunakan Theme Tokens Langsung**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: tokens.colors.bg.card,
|
||||||
|
color: tokens.colors.text.primary,
|
||||||
|
padding: tokens.spacing.md,
|
||||||
|
borderRadius: tokens.radius.lg,
|
||||||
|
border: `1px solid ${tokens.colors.border.default}`,
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: tokens.typography.body.fz }}>
|
||||||
|
Konten dengan styling konsisten
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌓 Dark Mode Toggle
|
||||||
|
|
||||||
|
### Otomatis di Header
|
||||||
|
|
||||||
|
Dark mode toggle sudah terintegrasi di header admin dashboard. User bisa toggle dengan klik tombol 🌙/☀️.
|
||||||
|
|
||||||
|
### Manual Toggle
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { DarkModeToggle } from '@/components/admin/DarkModeToggle';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { isDark, toggle } = useDarkMode();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Current mode: {isDark ? 'Dark' : 'Light'}</p>
|
||||||
|
|
||||||
|
{/* Gunakan component toggle */}
|
||||||
|
<DarkModeToggle />
|
||||||
|
|
||||||
|
{/* Atau manual */}
|
||||||
|
<button onClick={toggle}>Toggle</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistensi
|
||||||
|
|
||||||
|
Dark mode preference disimpan di `localStorage` dengan key `darmasaba-admin-dark-mode`.
|
||||||
|
Preference akan tetap ada saat user refresh halaman atau kembali nanti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Contoh Penggunaan Lengkap
|
||||||
|
|
||||||
|
### Contoh 1: List Page dengan Table
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
import { UnifiedPageHeader, UnifiedText } from '@/components/admin/UnifiedTypography';
|
||||||
|
import UnifiedCard from '@/components/admin/UnifiedSurface';
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
import { Button, Table, TableTr, TableTh, TableTd } from '@mantine/core';
|
||||||
|
|
||||||
|
export default function DaftarBerita() {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Header Halaman */}
|
||||||
|
<UnifiedPageHeader
|
||||||
|
title="Daftar Berita"
|
||||||
|
subtitle="Kelola semua berita yang diterbitkan"
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
bg={tokens.colors.primary}
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${tokens.colors.primary}, ${isDark ? '#60A5FA' : '#4facfe'})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Tambah Berita
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Card untuk Table */}
|
||||||
|
<UnifiedCard>
|
||||||
|
<Table>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh style={{ backgroundColor: tokens.colors.bg.surface }}>
|
||||||
|
<UnifiedText size="label" color="secondary">Judul</UnifiedText>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ backgroundColor: tokens.colors.bg.surface }}>
|
||||||
|
<UnifiedText size="label" color="secondary">Kategori</UnifiedText>
|
||||||
|
</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{data.map((item) => (
|
||||||
|
<TableTr
|
||||||
|
key={item.id}
|
||||||
|
style={{
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: tokens.colors.bg.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableTd>
|
||||||
|
<UnifiedText size="body">{item.judul}</UnifiedText>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<UnifiedText size="small" color="secondary">
|
||||||
|
{item.kategori}
|
||||||
|
</UnifiedText>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</UnifiedCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contoh 2: Detail Page
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography';
|
||||||
|
import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface';
|
||||||
|
|
||||||
|
export default function DetailBerita({ data }) {
|
||||||
|
return (
|
||||||
|
<UnifiedCard>
|
||||||
|
<UnifiedCard.Header>
|
||||||
|
<UnifiedTitle order={3} color="brand">{data.judul}</UnifiedTitle>
|
||||||
|
</UnifiedCard.Header>
|
||||||
|
|
||||||
|
<UnifiedCard.Body>
|
||||||
|
<Box mb="md">
|
||||||
|
<UnifiedText size="label" color="muted">Kategori</UnifiedText>
|
||||||
|
<UnifiedText size="body" weight="medium">{data.kategori}</UnifiedText>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<UnifiedDivider variant="soft" />
|
||||||
|
|
||||||
|
<Box my="md">
|
||||||
|
<UnifiedText size="label" color="muted">Deskripsi</UnifiedText>
|
||||||
|
<UnifiedText size="body">{data.deskripsi}</UnifiedText>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<UnifiedDivider variant="soft" />
|
||||||
|
|
||||||
|
<Box mt="md">
|
||||||
|
<UnifiedText size="label" color="muted">Konten</UnifiedText>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: data.content }} />
|
||||||
|
</Box>
|
||||||
|
</UnifiedCard.Body>
|
||||||
|
|
||||||
|
<UnifiedCard.Footer>
|
||||||
|
<Group justify="right">
|
||||||
|
<Button color="red">Hapus</Button>
|
||||||
|
<Button color="green">Edit</Button>
|
||||||
|
</Group>
|
||||||
|
</UnifiedCard.Footer>
|
||||||
|
</UnifiedCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Mengedit Style
|
||||||
|
|
||||||
|
### Edit Warna Dark Mode
|
||||||
|
|
||||||
|
File: `src/utils/themeTokens.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const darkColors = {
|
||||||
|
// Background Layers
|
||||||
|
bgBase: '#0B1220', // ← Edit di sini
|
||||||
|
bgApp: '#0F172A',
|
||||||
|
bgCard: '#162235',
|
||||||
|
bgSurface: '#1E2A3D',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
textPrimary: '#E5E7EB', // ← Edit di sini
|
||||||
|
textSecondary: '#9CA3AF',
|
||||||
|
|
||||||
|
// Accent
|
||||||
|
primaryAction: '#3B82F6', // ← Edit primary color
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit Warna Light Mode
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const lightColors = {
|
||||||
|
bgBase: '#f6f9fc',
|
||||||
|
bgCard: '#ffffff',
|
||||||
|
textPrimary: '#1a1b1e',
|
||||||
|
primaryAction: baseColors['blue-button'], // Dari colors.ts
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit Typography
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
typography: {
|
||||||
|
h1: {
|
||||||
|
fz: '2rem', // ← Edit ukuran
|
||||||
|
fw: 700, // ← Edit weight
|
||||||
|
lh: 1.2, // ← Edit line height
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
fz: '1rem',
|
||||||
|
fw: 400,
|
||||||
|
lh: 1.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit Spacing & Radius
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
spacing: {
|
||||||
|
xs: '0.625rem', // 10px
|
||||||
|
sm: '1rem', // 16px
|
||||||
|
md: '1.5rem', // 24px
|
||||||
|
lg: '2rem', // 32px
|
||||||
|
}
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
sm: '0.5rem', // 8px
|
||||||
|
md: '0.75rem', // 12px
|
||||||
|
lg: '1rem', // 16px
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Migrasi
|
||||||
|
|
||||||
|
Komponen yang sudah diupdate dengan dark mode:
|
||||||
|
|
||||||
|
- ✅ `src/app/admin/layout.tsx`
|
||||||
|
- ✅ `src/app/admin/(dashboard)/_com/header.tsx`
|
||||||
|
- ✅ `src/app/admin/(dashboard)/_com/judulList.tsx`
|
||||||
|
- ✅ `src/app/admin/(dashboard)/_com/judulListTab.tsx`
|
||||||
|
- ✅ `src/components/admin/UnifiedTypography.tsx`
|
||||||
|
- ✅ `src/components/admin/UnifiedSurface.tsx`
|
||||||
|
- ✅ `src/components/admin/DarkModeToggle.tsx`
|
||||||
|
- ✅ `src/utils/themeTokens.ts`
|
||||||
|
|
||||||
|
Komponen yang perlu diupdate (TODO):
|
||||||
|
|
||||||
|
- [ ] Komponen di `src/app/admin/(dashboard)/desa/`
|
||||||
|
- [ ] Komponen di `src/app/admin/(dashboard)/ppid/`
|
||||||
|
- [ ] Komponen di `src/app/admin/(dashboard)/kesehatan/`
|
||||||
|
- [ ] Komponen di `src/app/admin/(dashboard)/pendidikan/`
|
||||||
|
- [ ] Komponen di `src/app/admin/(dashboard)/ekonomi/`
|
||||||
|
- [ ] Dan lain-lain...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Referensi
|
||||||
|
|
||||||
|
- [Dark Mode Specification](../../../darkMode.md) - Spesifikasi lengkap dark mode
|
||||||
|
- [Mantine Theme System](https://mantine.dev/theming/theme-object/)
|
||||||
|
- [Mantine Dark Mode](https://mantine.dev/theming/dark-mode/)
|
||||||
|
- [Valtio State Management](https://github.com/pmndrs/valtio)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Tips
|
||||||
|
|
||||||
|
1. **Selalu gunakan unified components** untuk konsistensi dark/light mode
|
||||||
|
2. **Edit di `themeTokens.ts`** untuk perubahan global
|
||||||
|
3. **Test dark mode** setelah perubahan style
|
||||||
|
4. **Gunakan color semantic** (`primary`, `secondary`, `muted`) bukan hex langsung
|
||||||
|
5. **Jangan hardcode shadow** di dark mode (spec: "Jangan pakai shadow hitam")
|
||||||
|
6. **Border harus terlihat** di dark mode (opacity > 20%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
### Style tidak berubah setelah edit themeTokens.ts?
|
||||||
|
|
||||||
|
1. Clear browser cache (Cmd+Shift+R / Ctrl+Shift+R)
|
||||||
|
2. Restart dev server: `bun run dev`
|
||||||
|
3. Pastikan komponen menggunakan unified components
|
||||||
|
|
||||||
|
### Dark mode tidak berfungsi?
|
||||||
|
|
||||||
|
1. Cek `darkModeStore.ts` sudah diimport
|
||||||
|
2. Pastikan `useDarkMode()` hook digunakan
|
||||||
|
3. Clear localStorage: `localStorage.clear()`
|
||||||
|
4. Cek console untuk error
|
||||||
|
|
||||||
|
### Border tidak terlihat di dark mode?
|
||||||
|
|
||||||
|
Pastikan menggunakan `tokens.colors.border.default` atau `tokens.colors.border.soft`, bukan hardcode warna.
|
||||||
|
|
||||||
|
### Component tidak re-render?
|
||||||
|
|
||||||
|
1. Pastikan `'use client'` ada di file component
|
||||||
|
2. Gunakan `useSnapshot()` jika menggunakan Valtio di non-event handler
|
||||||
|
3. Cek console untuk error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 Spesifikasi Dark Mode
|
||||||
|
|
||||||
|
Untuk spesifikasi lengkap dark mode (layout rules, table styles, button rules, dll), lihat:
|
||||||
|
**[`darkMode.md`](../../../darkMode.md)**
|
||||||
|
|
||||||
|
Highlights:
|
||||||
|
- ✅ Background layers berbeda (base, app, card, surface)
|
||||||
|
- ✅ Border wajib terlihat (tidak flat)
|
||||||
|
- ✅ Active state dengan accent bar (2-3px)
|
||||||
|
- ✅ Tidak pakai shadow hitam
|
||||||
|
- ✅ Hover state dengan background soft
|
||||||
|
- ✅ Text kontras terbaca
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** February 20, 2026
|
||||||
|
**Version:** 2.0.0 (Dark Mode Ready)
|
||||||
|
**Based on:** darkMode.md specification
|
||||||
252
src/components/admin/UnifiedSurface.tsx
Normal file
252
src/components/admin/UnifiedSurface.tsx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
import { Paper, Box, BoxProps, Divider, DividerProps } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified Surface Components
|
||||||
|
*
|
||||||
|
* Komponen container/card dengan styling konsisten
|
||||||
|
* Mendukung dark mode sesuai spesifikasi darkMode.md
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { UnifiedCard, UnifiedDivider } from '@/components/admin/UnifiedSurface';
|
||||||
|
*
|
||||||
|
* <UnifiedCard>
|
||||||
|
* <UnifiedCard.Header>Title</UnifiedCard.Header>
|
||||||
|
* <UnifiedCard.Body>Content</UnifiedCard.Body>
|
||||||
|
* </UnifiedCard>
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unified Card Component
|
||||||
|
* ============================================================================
|
||||||
|
|
||||||
|
interface UnifiedCardProps extends BoxProps {
|
||||||
|
withBorder?: boolean;
|
||||||
|
shadow?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
|
padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
hoverable?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnifiedCard({
|
||||||
|
withBorder = true,
|
||||||
|
shadow = 'none', // Sesuai spec: Jangan pakai shadow hitam
|
||||||
|
padding = 'md',
|
||||||
|
hoverable = false,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: UnifiedCardProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const getPadding = () => {
|
||||||
|
switch (padding) {
|
||||||
|
case 'none':
|
||||||
|
return 0;
|
||||||
|
case 'xs':
|
||||||
|
return tokens.spacing.xs;
|
||||||
|
case 'sm':
|
||||||
|
return tokens.spacing.sm;
|
||||||
|
case 'md':
|
||||||
|
return tokens.spacing.md;
|
||||||
|
case 'lg':
|
||||||
|
return tokens.spacing.lg;
|
||||||
|
case 'xl':
|
||||||
|
return tokens.spacing.xl;
|
||||||
|
default:
|
||||||
|
return tokens.spacing.md;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
withBorder={withBorder}
|
||||||
|
bg={tokens.colors.bg.card}
|
||||||
|
p={getPadding()}
|
||||||
|
radius={tokens.radius.lg} // 12-16px sesuai spec
|
||||||
|
style={{
|
||||||
|
borderColor: tokens.colors.border.default,
|
||||||
|
transition: hoverable
|
||||||
|
? 'transform 0.2s ease, box-shadow 0.2s ease'
|
||||||
|
: 'box-shadow 0.2s ease',
|
||||||
|
'&:hover': hoverable
|
||||||
|
? {
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unified Card Section Components
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface UnifiedCardSectionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
border?: 'none' | 'top' | 'bottom';
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnifiedCard.Header = function UnifiedCardHeader({
|
||||||
|
children,
|
||||||
|
padding = 'md',
|
||||||
|
border = 'bottom',
|
||||||
|
style,
|
||||||
|
}: UnifiedCardSectionProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const getPadding = () => {
|
||||||
|
switch (padding) {
|
||||||
|
case 'none':
|
||||||
|
return 0;
|
||||||
|
case 'xs':
|
||||||
|
return tokens.spacing.xs;
|
||||||
|
case 'sm':
|
||||||
|
return tokens.spacing.sm;
|
||||||
|
case 'md':
|
||||||
|
return tokens.spacing.md;
|
||||||
|
case 'lg':
|
||||||
|
return tokens.spacing.lg;
|
||||||
|
default:
|
||||||
|
return tokens.spacing.md;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none';
|
||||||
|
const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
paddingBottom: getPadding(),
|
||||||
|
borderBottom,
|
||||||
|
borderTop,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UnifiedCard.Body = function UnifiedCardBody({
|
||||||
|
children,
|
||||||
|
padding = 'md',
|
||||||
|
style,
|
||||||
|
}: UnifiedCardSectionProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const getPadding = () => {
|
||||||
|
switch (padding) {
|
||||||
|
case 'none':
|
||||||
|
return 0;
|
||||||
|
case 'xs':
|
||||||
|
return tokens.spacing.xs;
|
||||||
|
case 'sm':
|
||||||
|
return tokens.spacing.sm;
|
||||||
|
case 'md':
|
||||||
|
return tokens.spacing.md;
|
||||||
|
case 'lg':
|
||||||
|
return tokens.spacing.lg;
|
||||||
|
default:
|
||||||
|
return tokens.spacing.md;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box style={{ paddingTop: getPadding(), paddingBottom: getPadding(), ...style }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UnifiedCard.Footer = function UnifiedCardFooter({
|
||||||
|
children,
|
||||||
|
padding = 'md',
|
||||||
|
border = 'top',
|
||||||
|
style,
|
||||||
|
}: UnifiedCardSectionProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const getPadding = () => {
|
||||||
|
switch (padding) {
|
||||||
|
case 'none':
|
||||||
|
return 0;
|
||||||
|
case 'xs':
|
||||||
|
return tokens.spacing.xs;
|
||||||
|
case 'sm':
|
||||||
|
return tokens.spacing.sm;
|
||||||
|
case 'md':
|
||||||
|
return tokens.spacing.md;
|
||||||
|
case 'lg':
|
||||||
|
return tokens.spacing.lg;
|
||||||
|
default:
|
||||||
|
return tokens.spacing.md;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none';
|
||||||
|
const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
paddingTop: getPadding(),
|
||||||
|
borderBottom,
|
||||||
|
borderTop,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unified Divider Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface UnifiedDividerProps extends DividerProps {
|
||||||
|
variant?: 'default' | 'soft' | 'strong';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnifiedDivider({
|
||||||
|
variant = 'soft', // Default soft sesuai spec
|
||||||
|
my = 'md',
|
||||||
|
...props
|
||||||
|
}: UnifiedDividerProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const getColor = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'default':
|
||||||
|
return tokens.colors.border.default;
|
||||||
|
case 'soft':
|
||||||
|
return tokens.colors.border.soft;
|
||||||
|
case 'strong':
|
||||||
|
return tokens.colors.border.strong;
|
||||||
|
default:
|
||||||
|
return tokens.colors.border.soft;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Divider my={my} color={getColor()} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnifiedCard;
|
||||||
268
src/components/admin/UnifiedTypography.tsx
Normal file
268
src/components/admin/UnifiedTypography.tsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useDarkMode } from '@/state/darkModeStore';
|
||||||
|
import { themeTokens, getResponsiveFz } from '@/utils/themeTokens';
|
||||||
|
import { Text, Title, Box, BoxProps } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified Typography Components
|
||||||
|
*
|
||||||
|
* Komponen text dengan styling konsisten di seluruh aplikasi
|
||||||
|
* Mendukung dark mode sesuai spesifikasi darkMode.md
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { UnifiedText, UnifiedTitle } from '@/components/admin/UnifiedTypography';
|
||||||
|
*
|
||||||
|
* <UnifiedTitle order={1}>Judul Halaman</UnifiedTitle>
|
||||||
|
* <UnifiedText size="body">Konten teks</UnifiedText>
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unified Title Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface UnifiedTitleProps {
|
||||||
|
order?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
children: React.ReactNode;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
color?: 'primary' | 'secondary' | 'brand' | string;
|
||||||
|
mb?: string;
|
||||||
|
mt?: string;
|
||||||
|
ml?: string;
|
||||||
|
mr?: string;
|
||||||
|
mx?: string;
|
||||||
|
my?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnifiedTitle({
|
||||||
|
order = 1,
|
||||||
|
children,
|
||||||
|
align = 'left',
|
||||||
|
color = 'primary',
|
||||||
|
mb,
|
||||||
|
mt,
|
||||||
|
ml,
|
||||||
|
mr,
|
||||||
|
mx,
|
||||||
|
my,
|
||||||
|
style,
|
||||||
|
}: UnifiedTitleProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
const responsiveFz = getResponsiveFz(isDark);
|
||||||
|
|
||||||
|
const getTypography = () => {
|
||||||
|
switch (order) {
|
||||||
|
case 1:
|
||||||
|
return tokens.typography.h1;
|
||||||
|
case 2:
|
||||||
|
return tokens.typography.h2;
|
||||||
|
case 3:
|
||||||
|
return tokens.typography.h3;
|
||||||
|
case 4:
|
||||||
|
return tokens.typography.h4;
|
||||||
|
default:
|
||||||
|
return tokens.typography.body;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const typo = getTypography();
|
||||||
|
|
||||||
|
const getColor = () => {
|
||||||
|
if (color === 'primary') return tokens.colors.text.primary;
|
||||||
|
if (color === 'secondary') return tokens.colors.text.secondary;
|
||||||
|
if (color === 'brand') return tokens.colors.brand;
|
||||||
|
return color;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Title
|
||||||
|
order={order}
|
||||||
|
ta={align}
|
||||||
|
fz={{ base: responsiveFz.base, md: typo.fz }}
|
||||||
|
fw={typo.fw}
|
||||||
|
lh={typo.lh}
|
||||||
|
c={getColor()}
|
||||||
|
mb={mb}
|
||||||
|
mt={mt}
|
||||||
|
ml={ml}
|
||||||
|
mr={mr}
|
||||||
|
mx={mx}
|
||||||
|
my={my}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Title>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unified Text Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface UnifiedTextProps {
|
||||||
|
size?: 'small' | 'body' | 'label';
|
||||||
|
weight?: 'normal' | 'medium' | 'bold';
|
||||||
|
children: React.ReactNode;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
color?: 'primary' | 'secondary' | 'tertiary' | 'muted' | 'brand' | 'link' | string;
|
||||||
|
lineClamp?: number;
|
||||||
|
truncate?: 'start' | 'end' | 'middle' | boolean;
|
||||||
|
span?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnifiedText({
|
||||||
|
size = 'body',
|
||||||
|
weight = 'normal',
|
||||||
|
children,
|
||||||
|
align = 'left',
|
||||||
|
color = 'primary',
|
||||||
|
lineClamp,
|
||||||
|
truncate,
|
||||||
|
span = false,
|
||||||
|
style,
|
||||||
|
}: UnifiedTextProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
const getTypography = () => {
|
||||||
|
switch (size) {
|
||||||
|
case 'small':
|
||||||
|
return tokens.typography.small;
|
||||||
|
case 'label':
|
||||||
|
return tokens.typography.label;
|
||||||
|
default:
|
||||||
|
return tokens.typography.body;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeight = () => {
|
||||||
|
switch (weight) {
|
||||||
|
case 'normal':
|
||||||
|
return 400;
|
||||||
|
case 'medium':
|
||||||
|
return 500;
|
||||||
|
case 'bold':
|
||||||
|
return 700;
|
||||||
|
default:
|
||||||
|
return 400;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColor = () => {
|
||||||
|
switch (color) {
|
||||||
|
case 'primary':
|
||||||
|
return tokens.colors.text.primary;
|
||||||
|
case 'secondary':
|
||||||
|
return tokens.colors.text.secondary;
|
||||||
|
case 'tertiary':
|
||||||
|
return tokens.colors.text.tertiary;
|
||||||
|
case 'muted':
|
||||||
|
return tokens.colors.text.muted;
|
||||||
|
case 'brand':
|
||||||
|
return tokens.colors.brand;
|
||||||
|
case 'link':
|
||||||
|
return tokens.colors.text.link;
|
||||||
|
default:
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const typo = getTypography();
|
||||||
|
const fw = getWeight();
|
||||||
|
const textColor = getColor();
|
||||||
|
|
||||||
|
if (span) {
|
||||||
|
return (
|
||||||
|
<Text.Span
|
||||||
|
ta={align}
|
||||||
|
fz={typo.fz}
|
||||||
|
fw={fw}
|
||||||
|
lh={typo.lh}
|
||||||
|
c={textColor}
|
||||||
|
lineClamp={lineClamp}
|
||||||
|
truncate={truncate}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text.Span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
ta={align}
|
||||||
|
fz={typo.fz}
|
||||||
|
fw={fw}
|
||||||
|
lh={typo.lh}
|
||||||
|
c={textColor}
|
||||||
|
lineClamp={lineClamp}
|
||||||
|
truncate={truncate}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unified Page Header Component
|
||||||
|
//
|
||||||
|
// Header standar untuk setiap halaman admin
|
||||||
|
// Sesuai spesifikasi: Section Header dengan font weight lebih besar
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface UnifiedPageHeaderProps extends BoxProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
action?: React.ReactNode;
|
||||||
|
showBorder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnifiedPageHeader({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
action,
|
||||||
|
showBorder = true,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: UnifiedPageHeaderProps) {
|
||||||
|
const { isDark } = useDarkMode();
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
mb="lg"
|
||||||
|
style={{
|
||||||
|
borderBottom: showBorder ? `1px solid ${tokens.colors.border.soft}` : 'none',
|
||||||
|
paddingBottom: showBorder ? tokens.spacing.md : 0,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: tokens.spacing.md,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<UnifiedTitle order={3} color="primary">{title}</UnifiedTitle>
|
||||||
|
{subtitle && (
|
||||||
|
<UnifiedText size="small" color="secondary" mt="xs">
|
||||||
|
{subtitle}
|
||||||
|
</UnifiedText>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action && <div>{action}</div>}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnifiedText;
|
||||||
84
src/lib/api-auth.ts
Normal file
84
src/lib/api-auth.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Authentication helper untuk API endpoints
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { requireAuth } from "@/lib/api-auth";
|
||||||
|
*
|
||||||
|
* export default async function myEndpoint(context: Context) {
|
||||||
|
* const authResult = await requireAuth(context);
|
||||||
|
* if (!authResult.authenticated) {
|
||||||
|
* return authResult.response;
|
||||||
|
* }
|
||||||
|
* // Lanjut proses dengan authResult.user
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSession } from "@/lib/session";
|
||||||
|
|
||||||
|
export type AuthResult =
|
||||||
|
| { authenticated: true; user: any }
|
||||||
|
| { authenticated: false; response: Response };
|
||||||
|
|
||||||
|
export async function requireAuth(context: any): Promise<AuthResult> {
|
||||||
|
try {
|
||||||
|
// Cek session dari cookies
|
||||||
|
const session = await getSession();
|
||||||
|
|
||||||
|
if (!session || !session.user) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
response: new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Unauthorized - Silakan login terlebih dahulu"
|
||||||
|
}), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check jika user masih aktif
|
||||||
|
if (!session.user.isActive) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
response: new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Akun Anda tidak aktif. Hubungi administrator."
|
||||||
|
}), {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
user: session.user
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auth error:", error);
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
response: new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Authentication error"
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional auth - tidak error jika tidak authenticated
|
||||||
|
* Berguna untuk endpoint yang bisa diakses public atau private
|
||||||
|
*/
|
||||||
|
export async function optionalAuth(context: any): Promise<any> {
|
||||||
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
return session?.user || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/lib/session.ts
Normal file
68
src/lib/session.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Session helper menggunakan iron-session
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { getSession } from "@/lib/session";
|
||||||
|
*
|
||||||
|
* const session = await getSession();
|
||||||
|
* if (session?.user) {
|
||||||
|
* // User authenticated
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getIronSession } from 'iron-session';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
export type SessionData = {
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
roleId: number;
|
||||||
|
menuIds?: string[] | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Session = SessionData & {
|
||||||
|
save: () => Promise<void>;
|
||||||
|
destroy: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SESSION_OPTIONS = {
|
||||||
|
cookieName: 'desa-session',
|
||||||
|
password: process.env.SESSION_PASSWORD || 'default-password-change-in-production',
|
||||||
|
cookieOptions: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax' as const,
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getSession(): Promise<SessionData | null> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const session = await getIronSession<SessionData>(
|
||||||
|
cookieStore,
|
||||||
|
SESSION_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Session error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function destroySession(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const session = await getIronSession<SessionData>(
|
||||||
|
cookieStore,
|
||||||
|
SESSION_OPTIONS
|
||||||
|
);
|
||||||
|
await session.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Destroy session error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/state/darkModeStore.ts
Normal file
77
src/state/darkModeStore.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Dark Mode State Management
|
||||||
|
*
|
||||||
|
* Menggunakan Valtio untuk global state
|
||||||
|
* Persist ke localStorage
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { darkModeStore } from '@/state/darkModeStore';
|
||||||
|
*
|
||||||
|
* // Toggle
|
||||||
|
* darkModeStore.toggle();
|
||||||
|
*
|
||||||
|
* // Set explicitly
|
||||||
|
* darkModeStore.setDarkMode(true);
|
||||||
|
*
|
||||||
|
* // Get current state
|
||||||
|
* const isDark = darkModeStore.isDark;
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { proxy, useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'darmasaba-admin-dark-mode';
|
||||||
|
|
||||||
|
// Initialize from localStorage or default to light mode
|
||||||
|
const getInitialDarkMode = (): boolean => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored !== null) {
|
||||||
|
return stored === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to light mode for first-time users
|
||||||
|
// System preference is NOT used as default to ensure consistent UX
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DarkModeStore {
|
||||||
|
public isDark: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.isDark = getInitialDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggle() {
|
||||||
|
this.isDark = !this.isDark;
|
||||||
|
this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDarkMode(value: boolean) {
|
||||||
|
this.isDark = value;
|
||||||
|
this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
private persist() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(STORAGE_KEY, String(this.isDark));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create proxy instance
|
||||||
|
const store = new DarkModeStore();
|
||||||
|
|
||||||
|
export const darkModeStore = proxy(store);
|
||||||
|
|
||||||
|
// Hook untuk menggunakan dark mode state di React components
|
||||||
|
export const useDarkMode = () => {
|
||||||
|
const snapshot = useSnapshot(darkModeStore);
|
||||||
|
return {
|
||||||
|
isDark: snapshot.isDark,
|
||||||
|
toggle: () => darkModeStore.toggle(),
|
||||||
|
setDarkMode: (value: boolean) => darkModeStore.setDarkMode(value),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default darkModeStore;
|
||||||
31
src/styles/dark-mode-table.css
Normal file
31
src/styles/dark-mode-table.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Dark Mode Table Styles
|
||||||
|
*
|
||||||
|
* Override Mantine table hover styles untuk dark mode
|
||||||
|
* Agar teks putih tetap terlihat saat hover
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Dark mode table hover */
|
||||||
|
[data-mantine-color-scheme="dark"] {
|
||||||
|
/* Table hover */
|
||||||
|
.mantine-Table-tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table striped hover */
|
||||||
|
.mantine-Table-striped .mantine-Table-tr:nth-of-type(odd):hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table with column borders */
|
||||||
|
.mantine-Table-withColumnBorders .mantine-Table-tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode table hover - default Mantine behavior */
|
||||||
|
[data-mantine-color-scheme="light"] {
|
||||||
|
.mantine-Table-tr:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
383
src/utils/themeTokens.ts
Normal file
383
src/utils/themeTokens.ts
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
/**
|
||||||
|
* Unified Theme Tokens for Admin Dashboard
|
||||||
|
*
|
||||||
|
* Berdasarkan spesifikasi: darkMode.md
|
||||||
|
*
|
||||||
|
* Semua styling constants disimpan di sini untuk konsistensi
|
||||||
|
* Edit di sini = edit di seluruh aplikasi
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { themeTokens } from '@/utils/themeTokens';
|
||||||
|
*
|
||||||
|
* // Light mode (default)
|
||||||
|
* const tokens = themeTokens(false);
|
||||||
|
*
|
||||||
|
* // Dark mode
|
||||||
|
* const tokens = themeTokens(true);
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ThemeTokens = {
|
||||||
|
// Colors
|
||||||
|
colors: {
|
||||||
|
primary: string;
|
||||||
|
primaryLight: string;
|
||||||
|
primaryDark: string;
|
||||||
|
gradient: {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
// Backgrounds
|
||||||
|
bg: {
|
||||||
|
base: string;
|
||||||
|
main: string;
|
||||||
|
app: string;
|
||||||
|
surface: string;
|
||||||
|
surfaceElevated: string;
|
||||||
|
header: string;
|
||||||
|
navbar: string;
|
||||||
|
card: string;
|
||||||
|
hover: string;
|
||||||
|
tableHeader: string;
|
||||||
|
tableHover: string;
|
||||||
|
};
|
||||||
|
// Text
|
||||||
|
text: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
muted: string;
|
||||||
|
brand: string;
|
||||||
|
inverse: string;
|
||||||
|
link: string;
|
||||||
|
};
|
||||||
|
// Borders
|
||||||
|
border: {
|
||||||
|
default: string;
|
||||||
|
soft: string;
|
||||||
|
strong: string;
|
||||||
|
};
|
||||||
|
// Status
|
||||||
|
success: string;
|
||||||
|
warning: string;
|
||||||
|
error: string;
|
||||||
|
info: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
typography: {
|
||||||
|
h1: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
h2: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
h3: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
h4: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
body: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
small: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
label: {
|
||||||
|
fz: string;
|
||||||
|
fw: number;
|
||||||
|
lh: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
spacing: {
|
||||||
|
xs: string;
|
||||||
|
sm: string;
|
||||||
|
md: string;
|
||||||
|
lg: string;
|
||||||
|
xl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Border Radius
|
||||||
|
radius: {
|
||||||
|
sm: string;
|
||||||
|
md: string;
|
||||||
|
lg: string;
|
||||||
|
xl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shadows
|
||||||
|
shadows: {
|
||||||
|
none: string;
|
||||||
|
sm: string;
|
||||||
|
md: string;
|
||||||
|
lg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
layout: {
|
||||||
|
headerHeight: number;
|
||||||
|
navbarWidth: {
|
||||||
|
base: number;
|
||||||
|
sm: number;
|
||||||
|
lg: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const themeTokens = (isDark: boolean = false): ThemeTokens => {
|
||||||
|
// Base colors - tetap menggunakan colors.ts sebagai base untuk light mode
|
||||||
|
const baseColors = {
|
||||||
|
'orange': '#FCAE00',
|
||||||
|
'blue-button': '#0A4E78',
|
||||||
|
'blue-button-1': '#E5F2FA',
|
||||||
|
'blue-button-2': '#B8DAEF',
|
||||||
|
'blue-button-3': '#8AC1E3',
|
||||||
|
'blue-button-4': '#5DA9D8',
|
||||||
|
'blue-button-5': '#2F91CC',
|
||||||
|
'blue-button-6': '#083F61',
|
||||||
|
'blue-button-7': '#062F49',
|
||||||
|
'blue-button-8': '#041F32',
|
||||||
|
'blue-button-trans': '#628EC6',
|
||||||
|
'white-1': '#FBFBFC',
|
||||||
|
'white-trans-1': 'rgba(255, 255, 255, 0.5)',
|
||||||
|
'white-trans-2': 'rgba(255, 255, 255, 0.7)',
|
||||||
|
'white-trans-3': 'rgba(255, 255, 255, 0.9)',
|
||||||
|
'grey-1': '#F4F5F6',
|
||||||
|
'grey-2': '#CBCACD',
|
||||||
|
'Bg': '#D1d9e8',
|
||||||
|
'BG-trans': '#B1C5F2',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DARK MODE PALETTE
|
||||||
|
* Berdasarkan spesifikasi: darkMode.md
|
||||||
|
*/
|
||||||
|
const darkColors = {
|
||||||
|
// Background Layers
|
||||||
|
bgBase: '#0B1220',
|
||||||
|
bgApp: '#0F172A',
|
||||||
|
bgCard: '#162235',
|
||||||
|
bgSurface: '#1E2A3D',
|
||||||
|
|
||||||
|
// Borders
|
||||||
|
borderDefault: '#2A3A52',
|
||||||
|
borderSoft: '#22314A',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
textPrimary: '#E5E7EB',
|
||||||
|
textSecondary: '#9CA3AF',
|
||||||
|
textMuted: '#6B7280',
|
||||||
|
textInverse: '#020617',
|
||||||
|
|
||||||
|
// Accent & Actions
|
||||||
|
primaryAction: '#3B82F6',
|
||||||
|
primaryHover: '#2563EB',
|
||||||
|
primaryActive: '#1D4ED8',
|
||||||
|
link: '#60A5FA',
|
||||||
|
|
||||||
|
// Status
|
||||||
|
success: '#22C55E',
|
||||||
|
warning: '#FACC15',
|
||||||
|
error: '#EF4444',
|
||||||
|
info: '#38BDF8',
|
||||||
|
|
||||||
|
// Hover states
|
||||||
|
hoverSoft: 'rgba(255,255,255,0.03)',
|
||||||
|
hoverMedium: 'rgba(255,255,255,0.04)',
|
||||||
|
activeAccent: 'rgba(59,130,246,0.15)',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LIGHT MODE PALETTE
|
||||||
|
* Original light theme
|
||||||
|
*/
|
||||||
|
const lightColors = {
|
||||||
|
bgBase: '#f6f9fc',
|
||||||
|
bgApp: '#ffffff',
|
||||||
|
bgCard: '#ffffff',
|
||||||
|
bgSurface: '#f8fafc',
|
||||||
|
borderDefault: '#e2e8f0',
|
||||||
|
borderSoft: '#e9ecef',
|
||||||
|
textPrimary: '#1a1b1e',
|
||||||
|
textSecondary: '#495057',
|
||||||
|
textMuted: '#868e96',
|
||||||
|
textInverse: '#ffffff',
|
||||||
|
primaryAction: baseColors['blue-button'],
|
||||||
|
primaryHover: '#083F61',
|
||||||
|
primaryActive: '#062F49',
|
||||||
|
link: '#2563eb',
|
||||||
|
hoverSoft: 'rgba(25, 113, 194, 0.03)',
|
||||||
|
hoverMedium: 'rgba(25, 113, 194, 0.05)',
|
||||||
|
activeAccent: 'rgba(25, 113, 194, 0.1)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const current = isDark ? darkColors : lightColors;
|
||||||
|
|
||||||
|
return {
|
||||||
|
colors: {
|
||||||
|
primary: current.primaryAction,
|
||||||
|
primaryLight: isDark ? current.activeAccent : baseColors['blue-button-1'],
|
||||||
|
primaryDark: current.primaryActive,
|
||||||
|
gradient: {
|
||||||
|
from: current.primaryAction,
|
||||||
|
to: isDark ? '#60A5FA' : '#228be6',
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
base: current.bgBase,
|
||||||
|
main: isDark ? current.bgBase : 'linear-gradient(180deg, #fdfdfd, #f6f9fc)',
|
||||||
|
app: current.bgApp,
|
||||||
|
surface: current.bgSurface,
|
||||||
|
surfaceElevated: isDark ? '#253347' : '#ffffff',
|
||||||
|
header: isDark
|
||||||
|
? `linear-gradient(180deg, ${current.bgApp} 0%, ${current.bgBase} 100%)`
|
||||||
|
: 'linear-gradient(90deg, #ffffff, #f9fbff)',
|
||||||
|
navbar: current.bgApp,
|
||||||
|
card: current.bgCard,
|
||||||
|
hover: current.hoverMedium,
|
||||||
|
tableHeader: current.bgSurface,
|
||||||
|
tableHover: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: current.textPrimary,
|
||||||
|
secondary: current.textSecondary,
|
||||||
|
tertiary: current.textMuted,
|
||||||
|
muted: current.textMuted,
|
||||||
|
brand: current.primaryAction,
|
||||||
|
inverse: current.textInverse,
|
||||||
|
link: current.link,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
default: current.borderDefault,
|
||||||
|
soft: current.borderSoft,
|
||||||
|
strong: isDark ? '#3A4A62' : '#ced4da',
|
||||||
|
},
|
||||||
|
success: current.success,
|
||||||
|
warning: current.warning,
|
||||||
|
error: current.error,
|
||||||
|
info: current.info,
|
||||||
|
},
|
||||||
|
|
||||||
|
typography: {
|
||||||
|
h1: {
|
||||||
|
fz: isDark ? '2rem' : '2.25rem',
|
||||||
|
fw: 700,
|
||||||
|
lh: 1.2,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fz: isDark ? '1.75rem' : '2rem',
|
||||||
|
fw: 700,
|
||||||
|
lh: 1.25,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fz: isDark ? '1.5rem' : '1.75rem',
|
||||||
|
fw: 700,
|
||||||
|
lh: 1.3,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fz: isDark ? '1.25rem' : '1.5rem',
|
||||||
|
fw: 600,
|
||||||
|
lh: 1.35,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
fz: '1rem',
|
||||||
|
fw: 400,
|
||||||
|
lh: 1.5,
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
fz: '0.875rem',
|
||||||
|
fw: 400,
|
||||||
|
lh: 1.4,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fz: '0.75rem',
|
||||||
|
fw: 600,
|
||||||
|
lh: 1.4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
spacing: {
|
||||||
|
xs: '0.625rem',
|
||||||
|
sm: '1rem',
|
||||||
|
md: '1.5rem',
|
||||||
|
lg: '2rem',
|
||||||
|
xl: '2.5rem',
|
||||||
|
},
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
sm: '0.5rem', // 8px
|
||||||
|
md: '0.75rem', // 12px
|
||||||
|
lg: '1rem', // 16px
|
||||||
|
xl: '1.25rem', // 20px
|
||||||
|
},
|
||||||
|
|
||||||
|
shadows: {
|
||||||
|
none: 'none',
|
||||||
|
sm: isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
md: isDark ? '0 4px 6px rgba(0,0,0,0.3)' : '0 4px 6px rgba(0,0,0,0.1)',
|
||||||
|
lg: isDark ? '0 10px 15px rgba(0,0,0,0.3)' : '0 10px 15px rgba(0,0,0,0.1)',
|
||||||
|
},
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
headerHeight: 64,
|
||||||
|
navbarWidth: {
|
||||||
|
base: 260,
|
||||||
|
sm: 280,
|
||||||
|
lg: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export default theme instances
|
||||||
|
export const lightTheme = themeTokens(false);
|
||||||
|
export const darkTheme = themeTokens(true);
|
||||||
|
|
||||||
|
// Helper untuk mendapatkan responsive font size
|
||||||
|
export const getResponsiveFz = (isDark: boolean = false) => ({
|
||||||
|
base: isDark ? 'md' : 'lg',
|
||||||
|
md: isDark ? 'lg' : 'xl',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper untuk mendapatkan color berdasarkan state
|
||||||
|
export const getActiveColor = (isActive: boolean, isDark: boolean = false) =>
|
||||||
|
isActive ? themeTokens(isDark).colors.primary : isDark ? themeTokens(isDark).colors.text.secondary : 'gray';
|
||||||
|
|
||||||
|
// Helper untuk mendapatkan background hover
|
||||||
|
export const getHoverBackground = (isActive: boolean, isDark: boolean = false) => {
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
return isActive
|
||||||
|
? tokens.colors.bg.hover
|
||||||
|
: tokens.colors.bg.hover;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper untuk active state dengan accent bar (sidebar)
|
||||||
|
export const getActiveStateStyles = (isActive: boolean, isDark: boolean = false) => {
|
||||||
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return {
|
||||||
|
backgroundColor: isDark ? tokens.colors.bg.hover : 'rgba(25, 113, 194, 0.1)',
|
||||||
|
borderLeft: isDark ? `3px solid ${tokens.colors.primary}` : '3px solid #1971c2',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: tokens.colors.bg.hover,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user