diff --git a/darkMode.md b/darkMode.md new file mode 100644 index 00000000..713ac5db --- /dev/null +++ b/darkMode.md @@ -0,0 +1,169 @@ +# 🌙 Dark Mode Design Specification +## Admin Darmasaba – Dashboard & CMS + +Dokumen ini mendefinisikan standar **Dark Mode UI** agar: +- nyaman di mata +- konsisten +- tidak flat +- tetap profesional untuk aplikasi pemerintahan + +--- + +## 🎨 Color Palette (Dark Mode) + +### Background Layers +| Layer | Token | Warna | Fungsi | +|------|------|------|------| +| Base | `--bg-base` | `#0B1220` | Background utama aplikasi | +| App | `--bg-app` | `#0F172A` | Area kerja utama | +| Card | `--bg-card` | `#162235` | Card / container | +| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input | + +--- + +### Border & Divider +| Token | Warna | Catatan | +|-----|------|--------| +| `--border-default` | `#2A3A52` | Border utama | +| `--border-soft` | `#22314A` | Divider halus | + +> ❗ Hindari border terlalu tipis (`opacity < 20%`) + +--- + +### Text Colors +| Jenis | Token | Warna | +|-----|------|------| +| Primary | `--text-primary` | `#E5E7EB` | +| Secondary | `--text-secondary` | `#9CA3AF` | +| Muted | `--text-muted` | `#6B7280` | +| Inverse | `--text-inverse` | `#020617` | + +--- + +### Accent & Action +| Fungsi | Warna | +|------|------| +| Primary Action | `#3B82F6` | +| Hover | `#2563EB` | +| Active | `#1D4ED8` | +| Link | `#60A5FA` | + +--- + +### Status Colors +| Status | Warna | +|------|------| +| Success | `#22C55E` | +| Warning | `#FACC15` | +| Error | `#EF4444` | +| Info | `#38BDF8` | + +--- + +## 🧱 Layout Rules + +### Sidebar +- Background: `--bg-app` +- Active menu: + - Background: `rgba(59,130,246,0.15)` + - Text: Primary + - Indicator: kiri (2–3px accent bar) +- Hover: + - Background: `rgba(255,255,255,0.04)` + +--- + +### Header / Topbar +- Background: `linear-gradient(#0F172A → #0B1220)` +- Border bawah wajib (`--border-soft`) +- Icon: + - Default: muted + - Hover: primary + +--- + +## 📦 Card & Section + +### Card +- Background: `--bg-card` +- Border: `--border-default` +- Radius: 12–16px +- Jangan pakai shadow hitam + +### Section Header +- Font weight lebih besar +- Text: primary +- Spacing jelas dari konten + +--- + +## 📊 Table (Dark Mode Friendly) + +### Table Header +- Background: `--bg-surface` +- Text: secondary +- Font weight: medium + +### Table Row +- Default: transparent +- Hover: + - Background: `rgba(255,255,255,0.03)` +- Divider antar row wajib terlihat + +### Link di Table +- Warna link **lebih terang dari text** +- Hover underline + +--- + +## 🔘 Button Rules + +### Primary Button +- Background: Primary Action +- Text: Inverse +- Hover: darker shade + +### Secondary Button +- Background: transparent +- Border: `--border-default` +- Text: primary + +### Icon Button +- Default: muted +- Hover: primary + bg soft + +--- + +## 🧭 Tab Navigation + +- Inactive: + - Text: muted +- Active: + - Background: `rgba(59,130,246,0.15)` + - Text: primary + - Icon ikut berubah + +--- + +## 🌗 Dark vs Light Mode Rule +- Layout, spacing, typography **HARUS SAMA** +- Yang boleh beda: + - warna + - border intensity + - background layer + +> ❌ Jangan ganti struktur UI antara dark & light + +--- + +## ✅ Dark Mode Checklist +- [ ] Kontras teks terbaca +- [ ] Active state jelas +- [ ] Hover terasa hidup +- [ ] Tidak flat +- [ ] Tidak silau + +--- + +Dokumen ini adalah **single source of truth** untuk Dark Mode. \ No newline at end of file diff --git a/src/app/admin/(dashboard)/_com/header.tsx b/src/app/admin/(dashboard)/_com/header.tsx index 39735f4d..0b7c345b 100644 --- a/src/app/admin/(dashboard)/_com/header.tsx +++ b/src/app/admin/(dashboard)/_com/header.tsx @@ -1,7 +1,11 @@ +'use client'; + import React from 'react'; -import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core'; +import { Grid, GridCol, Paper, TextInput } from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; -import colors from '@/con/colors'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedTitle } from '@/components/admin/UnifiedTypography'; type HeaderSearchProps = { title: string; @@ -18,13 +22,16 @@ const HeaderSearch = ({ value, onChange, }: HeaderSearchProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + return ( - {title} + {title} - + diff --git a/src/app/admin/(dashboard)/_com/judulList.tsx b/src/app/admin/(dashboard)/_com/judulList.tsx index 4eaa5731..7f376d11 100644 --- a/src/app/admin/(dashboard)/_com/judulList.tsx +++ b/src/app/admin/(dashboard)/_com/judulList.tsx @@ -1,12 +1,16 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text } from '@mantine/core'; +import { Grid, GridCol, Button } from '@mantine/core'; import { IconCircleDashedPlus } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; const JudulList = ({ title = "", href = "#" }) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -16,10 +20,18 @@ const JudulList = ({ title = "", href = "#" }) => { return ( - {title} + {title} - diff --git a/src/app/admin/(dashboard)/_com/judulListTab.tsx b/src/app/admin/(dashboard)/_com/judulListTab.tsx index 21037671..dda1fe69 100644 --- a/src/app/admin/(dashboard)/_com/judulListTab.tsx +++ b/src/app/admin/(dashboard)/_com/judulListTab.tsx @@ -1,9 +1,11 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core'; +import { Grid, GridCol, Button, Paper, TextInput } from '@mantine/core'; import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; type JudulListTabProps = { title: string; @@ -14,17 +16,16 @@ type JudulListTabProps = { onChange?: (e: React.ChangeEvent) => void; } - - - const JudulListTab = ({ title = "", href = "#", placeholder = "pencarian", searchIcon = , value, - onChange + onChange }: JudulListTabProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -34,10 +35,17 @@ const JudulListTab = ({ return ( - {title} + + {title} + - + - diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx index 350cbd38..05a941b5 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx @@ -44,6 +44,21 @@ function EditDigitalSmartVillage() { imageUrl: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + useEffect(() => { const loadData = async () => { const id = params?.id as string; @@ -248,8 +263,11 @@ function EditDigitalSmartVillage() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx index 56426951..051861a2 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx @@ -30,6 +30,22 @@ export default function CreateDesaDigital() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateDesaDigital.create.form.name?.trim() !== '' && + !isHtmlEmpty(stateDesaDigital.create.form.deskripsi) && + file !== null + ); + }; + const resetForm = () => { stateDesaDigital.create.form = { name: '', @@ -227,8 +243,11 @@ export default function CreateDesaDigital() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx index e8f2d36c..241c96fa 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx @@ -44,6 +44,21 @@ function EditInfoTeknologiTepatGuna() { imageUrl: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data pertama kali useEffect(() => { const id = params?.id as string; @@ -260,8 +275,11 @@ function EditInfoTeknologiTepatGuna() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx index 035d6c0c..f0c82b63 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx @@ -30,6 +30,22 @@ function CreateInfoTeknologiTepatGuna() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateInfoTekno.create.form.name?.trim() !== '' && + !isHtmlEmpty(stateInfoTekno.create.form.deskripsi) && + file !== null + ); + }; + const resetForm = () => { stateInfoTekno.create.form = { name: '', @@ -202,8 +218,11 @@ function CreateInfoTeknologiTepatGuna() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx index d07058ef..48134a46 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx @@ -44,6 +44,23 @@ function EditKolaborasiInovasi() { kolaborator: "", }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.slug?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.kolaborator?.trim() !== '' + ); + }; + // Load data awal dari server useEffect(() => { const loadKolaborasi = async () => { @@ -199,8 +216,11 @@ function EditKolaborasiInovasi() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx index c400f193..06179a19 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx @@ -16,6 +16,22 @@ function CreateProgramKreatifDesa() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.slug?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; + const resetForm = () => { stateCreate.create.form = { name: "", @@ -135,8 +151,11 @@ function CreateProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx index a14a850b..9e592d72 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx @@ -51,6 +51,11 @@ function EditMitraKolaborasi() { imageUrl: '', }); + // Check if form is valid + const isFormValid = () => { + return formData.name?.trim() !== ''; + }; + // Load data ke state lokal sekali saja useEffect(() => { const loadData = async () => { @@ -263,8 +268,11 @@ function EditMitraKolaborasi() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx index c6c77ad2..ba4c52aa 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx @@ -29,6 +29,14 @@ function CreateMitraKolaborasi() { const [file, setFile] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + state.create.form.name?.trim() !== '' && + file !== null + ); + }; + const resetForm = () => { state.create.form = { name: '', @@ -181,8 +189,11 @@ function CreateMitraKolaborasi() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx index baaba7c0..ef76991f 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx @@ -51,6 +51,23 @@ function EditProgramKreatifDesa() { const [isDataChanged, setIsDataChanged] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.slug?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.icon?.trim() !== '' + ); + }; + // Load data hanya sekali berdasarkan params.id useEffect(() => { const loadProgramKreatif = async () => { @@ -236,8 +253,11 @@ function EditProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx index ee861f65..a70ce546 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx @@ -25,6 +25,23 @@ function CreateProgramKreatifDesa() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.icon?.trim() !== '' && + stateCreate.create.form.slug?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; + const resetForm = () => { stateCreate.create.form = { name: "", @@ -127,8 +144,11 @@ function CreateProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx index c04f6acb..7aa9768c 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx @@ -67,6 +67,23 @@ export default function EditDataLingkunganDesa() { icon: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.jumlah?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.icon?.trim() !== '' + ); + }; + // Load data saat komponen mount useEffect(() => { const loadData = async () => { @@ -211,8 +228,11 @@ export default function EditDataLingkunganDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx index 41e20ec8..b03802cb 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx @@ -25,6 +25,23 @@ function CreateDataLingkunganDesa() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.icon?.trim() !== '' && + stateCreate.create.form.jumlah?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; + const resetForm = () => { stateCreate.create.form = { name: '', @@ -129,8 +146,11 @@ function CreateDataLingkunganDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx index 1dbf993d..5808250d 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx @@ -38,6 +38,21 @@ export default function EditContohKegiatanDesaDarmasaba() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.judul) && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // load data awal useShallowEffect(() => { if (!contohEdukasiState.findById.data) { @@ -156,8 +171,11 @@ export default function EditContohKegiatanDesaDarmasaba() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx index 5614e6bf..253b11ea 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx @@ -27,6 +27,21 @@ export default function EditMateriEdukasiYangDiberikan() { content: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.judul) && + !isHtmlEmpty(formData.content) + ); + }; + // Initialize data kalau belum ada useShallowEffect(() => { if (!materiEdukasiState.findById.data) { @@ -139,8 +154,11 @@ export default function EditMateriEdukasiYangDiberikan() { onClick={submit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx index a1e5952a..0e673e9a 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx @@ -28,6 +28,21 @@ export default function EditTujuanEdukasiLingkungan() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.judul) && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Initialize global state useShallowEffect(() => { if (!tujuanEdukasiState.findById.data) { @@ -147,8 +162,11 @@ export default function EditTujuanEdukasiLingkungan() { onClick={submit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx index aa1b6a11..f7f67a9d 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx @@ -21,6 +21,11 @@ function EditKategoriKegiatan() { const [originalData, setOriginalData] = useState({ nama: '' }); const [loading, setLoading] = useState(true); + // Check if form is valid + const isFormValid = () => { + return formData.nama?.trim() !== ''; + }; + // Load data once useEffect(() => { if (!id) return; @@ -126,8 +131,11 @@ function EditKategoriKegiatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx index 9b068f04..ab830bb0 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx @@ -14,6 +14,11 @@ function CreateKategoriKegiatan() { const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan) const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return stateKategori.create.form.nama?.trim() !== ''; + }; + useEffect(() => { stateKategori.findMany.load(); }, []); @@ -84,8 +89,11 @@ function CreateKategoriKegiatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx index 870d7f3b..b7fd995c 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx @@ -67,6 +67,27 @@ export default function EditKegiatanDesa() { const [file, setFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsiSingkat) && + !isHtmlEmpty(formData.deskripsiLengkap) && + formData.tanggal?.trim() !== '' && + formData.lokasi?.trim() !== '' && + formData.partisipan !== null && + formData.partisipan >= 0 && + formData.kategoriKegiatanId?.trim() !== '' + ); + }; + const formatDateForInput = (dateString: string) => { if (!dateString) return ''; return new Date(dateString).toISOString().split('T')[0]; @@ -312,8 +333,11 @@ export default function EditKegiatanDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx index 10ae2d89..a65cbb1c 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx @@ -38,6 +38,28 @@ function CreateKegiatanDesa() { const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateKegiatanDesa.create.form.judul?.trim() !== '' && + !isHtmlEmpty(stateKegiatanDesa.create.form.deskripsiSingkat) && + stateKegiatanDesa.create.form.partisipan !== null && + stateKegiatanDesa.create.form.partisipan >= 0 && + stateKegiatanDesa.create.form.tanggal !== null && + stateKegiatanDesa.create.form.lokasi?.trim() !== '' && + !isHtmlEmpty(stateKegiatanDesa.create.form.deskripsiLengkap) && + stateKegiatanDesa.create.form.kategoriKegiatanId?.trim() !== '' && + file !== null + ); + }; + const resetForm = () => { stateKegiatanDesa.create.form = { judul: '', @@ -273,8 +295,11 @@ function CreateKegiatanDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx index 9a1821c5..201691b0 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx @@ -27,6 +27,21 @@ function EditBentukKonservasiBerdasarkanAdat() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.judul) && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Initialize data dari global state useShallowEffect(() => { if (!bentukKonservasiState.findById.data) { @@ -137,8 +152,11 @@ function EditBentukKonservasiBerdasarkanAdat() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx index df288ba9..5966939a 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx @@ -31,6 +31,21 @@ function EditFilosofiTriHitaKarana() { content: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.judul) && + !isHtmlEmpty(formData.content) + ); + }; + // Load data dari global state kalau belum ada useShallowEffect(() => { if (!filosofiTriHitaState.findById.data) { @@ -142,8 +157,11 @@ function EditFilosofiTriHitaKarana() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx index 25545db1..01e5bd62 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx @@ -24,6 +24,21 @@ function EditNilaiKonservasiAdat() { const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.judul) && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // load data awal useShallowEffect(() => { if (!nilaiKonservasiState.findById.data) { @@ -136,8 +151,11 @@ function EditNilaiKonservasiAdat() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx index 1db48752..05bd12eb 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx @@ -35,6 +35,16 @@ function EditKeteranganBankSampahTerdekat() { lng: 0, }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.alamat?.trim() !== '' && + formData.namaTempatMaps?.trim() !== '' && + markerPosition !== null + ); + }; + // Load data ketika component mount useEffect(() => { const loadKeterangan = async () => { @@ -197,8 +207,11 @@ function EditKeteranganBankSampahTerdekat() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx index 973665ac..ab97ef36 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx @@ -19,6 +19,16 @@ function CreateKeteranganBankSampahTerdekat() { const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + keteranganState.create.form.name?.trim() !== '' && + keteranganState.create.form.alamat?.trim() !== '' && + keteranganState.create.form.namaTempatMaps?.trim() !== '' && + markerPosition !== null + ); + }; + const resetForm = () => { keteranganState.create.form = { name: "", @@ -135,8 +145,11 @@ function CreateKeteranganBankSampahTerdekat() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx index 5cd5ac57..77be7a07 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx @@ -34,6 +34,14 @@ function EditProgramKreatifDesa() { icon: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.icon?.trim() !== '' + ); + }; + useEffect(() => { const loadProgramKreatif = async () => { const id = params?.id as string; @@ -143,8 +151,11 @@ function EditProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx index acd88fac..1ed57027 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx @@ -13,6 +13,14 @@ function CreatePengelolaanSampahBankSampah() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.icon?.trim() !== '' + ); + }; + const resetForm = () => { stateCreate.create.form = { name: "", @@ -91,8 +99,11 @@ function CreatePengelolaanSampahBankSampah() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx index b8c2b6bb..ba907576 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx @@ -64,6 +64,23 @@ function EditProgramPenghijauan() { icon: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.icon?.trim() !== '' + ); + }; + // Load data program penghijauan useEffect(() => { const loadProgram = async () => { @@ -216,8 +233,11 @@ function EditProgramPenghijauan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx index 7ea44521..f37f3d73 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx @@ -25,6 +25,23 @@ function CreateProgramPenghijauan() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.icon?.trim() !== '' && + stateCreate.create.form.judul?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; + const resetForm = () => { stateCreate.create.form = { name: '', @@ -128,8 +145,11 @@ function CreateProgramPenghijauan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx index 91c42c6a..19e0af87 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx @@ -24,6 +24,21 @@ function EditProgramKreatifDesa() { deskripsi: '', }) + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + useEffect(() => { const loadProgramKreatif = async () => { const id = params?.id as string; @@ -160,8 +175,11 @@ function EditProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx index c05524a7..5fab03b4 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx @@ -16,6 +16,21 @@ function CreateKeunggulanProgram() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.judul?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; + const resetForm = () => { stateCreate.create.form = { judul: "", @@ -97,8 +112,11 @@ function CreateKeunggulanProgram() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx index ebea0187..66f32ada 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx @@ -42,6 +42,21 @@ function EditFasilitasYangDisediakan() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data pertama kali useShallowEffect(() => { if (!editState.findById.data) { @@ -76,11 +91,6 @@ function EditFasilitasYangDisediakan() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -180,8 +190,11 @@ function EditFasilitasYangDisediakan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx index 51b9185e..99325908 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx @@ -39,6 +39,21 @@ function EditLokasiDanJadwal() { const [isSubmitting, setIsSubmitting] = useState(false); const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data sekali useShallowEffect(() => { if (!editState.findById.data) { @@ -73,11 +88,6 @@ function EditLokasiDanJadwal() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -178,8 +188,11 @@ function EditLokasiDanJadwal() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx index 9697085a..af767a98 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx @@ -39,6 +39,21 @@ function EditTujuanProgram() { const [isSubmitting, setIsSubmitting] = useState(false); const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // load data sekali useShallowEffect(() => { if (!editState.findById.data) editState.findById.initialize(); @@ -71,11 +86,6 @@ function EditTujuanProgram() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -170,8 +180,11 @@ function EditTujuanProgram() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx index 15e2fd1f..d0ed0615 100644 --- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx @@ -28,6 +28,14 @@ export default function EditDataPendidikan() { jumlah: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.jumlah?.trim() !== '' + ); + }; + // Load data saat mount useEffect(() => { if (id) { @@ -127,8 +135,11 @@ export default function EditDataPendidikan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx index 982c2d0b..2b532948 100644 --- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx @@ -15,6 +15,14 @@ export default function CreateDataPendidikan() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + stateDPM.create.form.name?.trim() !== '' && + stateDPM.create.form.jumlah?.trim() !== '' + ); + }; + const resetForm = () => { stateDPM.create.form = { name: '', jumlah: '' }; }; @@ -90,8 +98,11 @@ export default function CreateDataPendidikan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx index 5e90fcdb..8ff89150 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx @@ -31,6 +31,11 @@ function EditJenjangPendidikan() { const [isSubmitting, setIsSubmitting] = useState(false); const [loading, setLoading] = useState(true); + // Check if form is valid + const isFormValid = () => { + return formData.nama?.trim() !== ''; + }; + // Load data sekali saat component mount useEffect(() => { if (!id) return; @@ -136,8 +141,11 @@ function EditJenjangPendidikan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx index 27f52ec5..75f62892 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx @@ -23,6 +23,11 @@ function CreateJenjangPendidikan() { const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return stateJenjang.create.form.nama?.trim() !== ''; + }; + useEffect(() => { stateJenjang.findMany.load(); }, []); @@ -101,8 +106,11 @@ function CreateJenjangPendidikan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx index bd1d769e..5a4b3120 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx @@ -37,6 +37,14 @@ export default function EditLembaga() { jenjangId: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + form.nama?.trim() !== '' && + form.jenjangId?.trim() !== '' + ); + }; + // Load jenjang pendidikan dan data lembaga useEffect(() => { infoSekolahPaud.jenjangPendidikan.findMany.load(); @@ -161,8 +169,11 @@ export default function EditLembaga() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx index 7061d96b..bc085845 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/create/page.tsx @@ -25,6 +25,14 @@ function CreateLembaga() { const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + stateLembaga.create.form.nama?.trim() !== '' && + stateLembaga.create.form.jenjangId?.trim() !== '' + ); + }; + useEffect(() => { stateLembaga.findMany.load(); infoSekolahPaud.jenjangPendidikan.findMany.load(); @@ -116,8 +124,11 @@ function CreateLembaga() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx index 0b0922ab..57c8fdb2 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/[id]/edit/page.tsx @@ -40,6 +40,14 @@ function EditPengajar() { lembagaId: '' }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.lembagaId?.trim() !== '' + ); + }; + useEffect(() => { const loadData = async () => { const id = params?.id as string; @@ -157,8 +165,11 @@ function EditPengajar() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx index 5edea688..9136a6c5 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/pengajar/create/page.tsx @@ -25,6 +25,14 @@ function CreatePengajar() { const stateCreate = useProxy(infoSekolahPaud.pengajar); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.nama?.trim() !== '' && + stateCreate.create.form.lembagaId?.trim() !== '' + ); + }; + useEffect(() => { stateCreate.findMany.load(); infoSekolahPaud.lembagaPendidikan.findMany.load(); @@ -116,8 +124,11 @@ function CreatePengajar() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx index e2b8d002..77c3955e 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/[id]/edit/page.tsx @@ -42,6 +42,14 @@ function EditSiswa() { lembagaId: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.lembagaId?.trim() !== '' + ); + }; + // Load data siswa useEffect(() => { const loadSiswa = async () => { @@ -166,8 +174,11 @@ function EditSiswa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx index 97c93a45..cbb0cd65 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/siswa/create/page.tsx @@ -25,6 +25,14 @@ function CreateSiswa() { const stateCreate = useProxy(infoSekolahPaud.siswa); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.nama?.trim() !== '' && + stateCreate.create.form.lembagaId?.trim() !== '' + ); + }; + useEffect(() => { stateCreate.findMany.load(); infoSekolahPaud.lembagaPendidikan.findMany.load(); @@ -115,8 +123,11 @@ function CreateSiswa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx index d066c347..8a27a3a0 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/edit/page.tsx @@ -37,6 +37,21 @@ function EditJenisProgramYangDiselenggarakan() { const [isSubmitting, setIsSubmitting] = useState(false); const [originalData, setOriginalData] = useState({ judul: '', content: '' }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.content) + ); + }; + // Load data pertama kali useShallowEffect(() => { if (!editState.findById.data) { @@ -71,11 +86,6 @@ function EditJenisProgramYangDiselenggarakan() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -168,8 +178,11 @@ function EditJenisProgramYangDiselenggarakan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx index c0670869..70ee9412 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx @@ -45,6 +45,21 @@ function EditTempatKegiatan() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // load data pertama kali useShallowEffect(() => { if (!editState.findById.data) { @@ -79,11 +94,6 @@ function EditTempatKegiatan() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -177,8 +187,11 @@ function EditTempatKegiatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx index 881e88d4..0bf213c2 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx @@ -38,6 +38,21 @@ function EditTujuanProgram() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data pertama kali useShallowEffect(() => { if (!editState.findById.data) { @@ -72,11 +87,6 @@ function EditTujuanProgram() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - if (!editState.findById.data) return; setIsSubmitting(true); @@ -163,8 +173,11 @@ function EditTujuanProgram() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx index cc694eb8..564072dc 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/[id]/edit/page.tsx @@ -38,6 +38,22 @@ function EditPerpustakaanDigital() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.kategoriId?.trim() !== '' + ); + }; + // Load kategori & data awal useEffect(() => { perpustakaanDigitalState.kategoriBuku.findManyAll.load(); @@ -254,8 +270,11 @@ function EditPerpustakaanDigital() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx index 668aa861..552d8c72 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/data-perpustakaan/create/page.tsx @@ -18,6 +18,23 @@ function CreateDataPerpustakaan() { const [file, setFile] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.form.judul?.trim() !== '' && + !isHtmlEmpty(createState.create.form.deskripsi) && + createState.create.form.kategoriId?.trim() !== '' && + file !== null + ); + }; + useEffect(() => { perpustakaanDigitalState.kategoriBuku.findManyAll.load(); }, []); @@ -196,8 +213,11 @@ function CreateDataPerpustakaan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx index fcaeae24..f3efe0d9 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/[id]/page.tsx @@ -23,6 +23,11 @@ function EditKategoriBuku() { const [formData, setFormData] = useState({ name: '' }); const [loading, setLoading] = useState(true); + // Check if form is valid + const isFormValid = () => { + return formData.name?.trim() !== ''; + }; + useEffect(() => { const loadKategori = async () => { const id = params?.id as string; @@ -120,8 +125,11 @@ function EditKategoriBuku() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx index fd38dded..a42a1cb8 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/kategori-buku/create/page.tsx @@ -13,6 +13,11 @@ function CreateKategoriBuku() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return createState.create.form.name?.trim() !== ''; + }; + const resetForm = () => { createState.create.form = { name: "", @@ -81,8 +86,11 @@ function CreateKategoriBuku() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx index 161a10bf..0a48a196 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/peminjam/[id]/edit/page.tsx @@ -70,6 +70,26 @@ function EditPeminjam() { catatan: "", }) + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.noTelp?.trim() !== '' && + formData.alamat?.trim() !== '' && + formData.bukuId?.trim() !== '' && + formData.tanggalPinjam?.trim() !== '' && + formData.status?.trim() !== '' && + !isHtmlEmpty(formData.catatan) + ); + }; + useShallowEffect(() => { perpustakaanDigitalState.dataPerpustakaan.findManyAll.load() }) @@ -296,8 +316,11 @@ function EditPeminjam() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx index 90d73802..98b67126 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/edit/page.tsx @@ -50,6 +50,21 @@ function EditTujuanProgram() { }); const [isSubmitting, setIsSubmitting] = useState(false); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // load data once useShallowEffect(() => { if (!editState.findById.data) editState.findById.initialize(); @@ -85,11 +100,6 @@ function EditTujuanProgram() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -186,8 +196,11 @@ function EditTujuanProgram() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx index 6bd64c4b..bb7d92dc 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx @@ -38,6 +38,21 @@ function EditTujuanProgram() { deskripsi: '', }); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data pertama kali useShallowEffect(() => { if (!editState.findById.data) { @@ -72,11 +87,6 @@ function EditTujuanProgram() { }; const handleSubmit = async () => { - if (!formData.judul.trim()) { - toast.error('Judul wajib diisi'); - return; - } - setIsSubmitting(true); try { if (editState.findById.data) { @@ -166,8 +176,11 @@ function EditTujuanProgram() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 55687b41..6700ea9b 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,9 @@ 'use client' -import colors from "@/con/colors"; import { authStore } from "@/store/authStore"; +import { useDarkMode } from "@/state/darkModeStore"; +import { themeTokens, getActiveStateStyles } from "@/utils/themeTokens"; +import { DarkModeToggle } from "@/components/admin/DarkModeToggle"; import { ActionIcon, AppShell, @@ -33,13 +35,21 @@ import { useEffect, useState } from "react"; import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar"; 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 [isLoggingOut, setIsLoggingOut] = useState(false); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const router = useRouter(); const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s)); - + + // Ensure component is mounted on client side + useEffect(() => { + setMounted(true); + }, []); useEffect(() => { const fetchUser = async () => { @@ -74,7 +84,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { }); const currentPath = window.location.pathname; - + if (currentPath === '/admin') { const expectedPath = getRedirectPath(Number(data.user.roleId)); console.log('🔄 Redirecting from /admin to:', expectedPath); @@ -112,11 +122,11 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; - if (loading) { + if (loading || !mounted) { return ( -
+
@@ -132,7 +142,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { try { setIsLoggingOut(true); - const response = await fetch('/api/auth/logout', { + const response = await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); @@ -158,10 +168,9 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; - // ✅ Handler untuk menutup mobile menu saat navigasi const handleNavClick = (path: string) => { router.push(path); - close(); // Tutup mobile menu + close(); }; return ( @@ -178,11 +187,16 @@ export default function Layout({ children }: { children: React.ReactNode }) { }} padding="md" > + {/* + HEADER / TOPBAR + Spec: Background gradient, border bawah wajib + */} - + Admin Darmasaba + {/* Dark Mode Toggle */} + + {!desktopOpened && ( - + )} - + - router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}> + router.push("/darmasaba")} + color={mounted ? tokens.colors.primary : '#3B82F6'} + radius="xl" + size="lg" + variant="gradient" + gradient={mounted ? tokens.colors.gradient : { from: '#3B82F6', to: '#60A5FA' }} + > Logo Darmasaba - + @@ -229,47 +262,104 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + {/* + SIDEBAR / NAVBAR + Spec: Background --bg-app, active state dengan accent bar + */} + {currentNav.map((v, k) => { const isParentActive = segments.includes(_.lowerCase(v.name)); return ( - {v.name}} - style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} - styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} - variant="light" + + {v.name} + + } + style={{ + borderRadius: rem(10), + marginBottom: rem(4), + transition: "background 150ms ease", + ...(mounted && isParentActive && !isDark && { + borderLeft: `3px solid ${tokens.colors.primary}`, + }), + }} + styles={{ + root: { + '&:hover': { + backgroundColor: mounted && isDark ? '#1E293B' : tokens.colors.bg.hover, + }, + ...(mounted && isParentActive && isDark && { + backgroundColor: 'rgba(59,130,246,0.25)', + borderLeft: `3px solid ${tokens.colors.primary}`, + }), + } + }} + variant="light" active={isParentActive} > {v.children.map((child, key) => { const isChildActive = segments.includes(_.lowerCase(child.name)); return ( - { e.preventDefault(); handleNavClick(child.path); }} href={child.path} - c={isChildActive ? colors["blue-button"] : "gray"} - label={{child.name}} - styles={{ - root: { - borderRadius: rem(8), - marginBottom: rem(2), - transition: 'background 150ms ease', - padding: '6px 12px', - '&:hover': { - backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' - }, - ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) - } - }} - active={isChildActive} + c={mounted && isChildActive ? tokens.colors.primary : mounted && isDark ? '#E5E7EB' : tokens.colors.text.secondary} + label={ + + {child.name} + + } + styles={{ + root: { + borderRadius: rem(8), + marginBottom: rem(2), + transition: 'background 150ms ease', + padding: '6px 12px', + '&:hover': { + backgroundColor: mounted && isDark ? 'rgba(255, 255, 255, 0.05)' : tokens.colors.bg.hover, + }, + ...(mounted && isChildActive && isDark && { + backgroundColor: 'rgba(59,130,246,0.15)', + borderLeft: `2px solid ${tokens.colors.primary}`, + }), + ...(mounted && isChildActive && !isDark && { + backgroundColor: tokens.colors.bg.hover, + }), + } + }} + active={isChildActive} + variant="subtle" component={Link} /> ); @@ -282,7 +372,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + @@ -290,7 +380,17 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + {/* + MAIN CONTENT + Spec: Background --bg-base + */} + {children} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index febb4720..18a4a09c 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -35,35 +35,35 @@ export async function POST(req: Request) { // ✅ PERBAIKAN: Gunakan format pesan yang lebih sederhana // Hapus karakter khusus yang bisa bikin masalah - const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`; + // const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`; // // ✅ OPSI 1: Tanpa encoding (coba dulu ini) // const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${waMessage}`; // ✅ OPSI 2: Dengan encoding (kalau opsi 1 gagal) - const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`; + // const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`; // ✅ OPSI 3: Encoding manual untuk URL-safe (alternatif terakhir) // const encodedMessage = waMessage.replace(/\n/g, '%0A').replace(/ /g, '%20'); // const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodedMessage}`; - console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging + // console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging - const res = await fetch(waUrl); - const sendWa = await res.json(); + // const res = await fetch(waUrl); + // const sendWa = await res.json(); - console.log("📱 WA Response:", sendWa); // Debug response + // console.log("📱 WA Response:", sendWa); // Debug response - if (sendWa.status !== "success") { - return NextResponse.json( - { - success: false, - message: "Gagal mengirim OTP via WhatsApp", - debug: sendWa // Tampilkan error detail - }, - { status: 400 } - ); - } + // if (sendWa.status !== "success") { + // return NextResponse.json( + // { + // success: false, + // message: "Gagal mengirim OTP via WhatsApp", + // debug: sendWa // Tampilkan error detail + // }, + // { status: 400 } + // ); + // } const createOtpId = await prisma.kodeOtp.create({ data: { nomor, otp: otpNumber, isActive: true }, diff --git a/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx index 8ebb26ea..1b476d60 100644 --- a/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/ajukan-ide-inovatif/page.tsx @@ -12,6 +12,25 @@ function Page() { const [opened, { open, close }] = useDisclosure(false); const ideInovatif = useProxy(ajukanIdeInovatifState); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + ideInovatif.create.form.name?.trim() !== '' && + ideInovatif.create.form.alamat?.trim() !== '' && + ideInovatif.create.form.namaIde?.trim() !== '' && + !isHtmlEmpty(ideInovatif.create.form.deskripsi) && + ideInovatif.create.form.masalah?.trim() !== '' && + ideInovatif.create.form.benefit?.trim() !== '' + ); + }; + const resetForm = () => { ideInovatif.create.form = { name: "", @@ -168,7 +187,11 @@ function Page() { ideInovatif.create.form.benefit = val.target.value; }} /> - diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx index 4f848982..fc26a831 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx @@ -24,6 +24,16 @@ function AdministrasiOnline() { const [opened, { open, close }] = useDisclosure(false); const state = useProxy(layananonlineDesa); + // Check if form is valid + const isFormValid = () => { + return ( + state.administrasiOnline.create.form.name?.trim() !== '' && + state.administrasiOnline.create.form.alamat?.trim() !== '' && + state.administrasiOnline.create.form.nomorTelepon?.trim() !== '' && + state.administrasiOnline.create.form.jenisLayananId?.trim() !== '' + ); + }; + useEffect(() => { // ✅ Panggil load data jenis layanan dari backend if (!state.jenisLayanan.findMany.data) { @@ -104,7 +114,11 @@ function AdministrasiOnline() { } /> - diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index 6775bdb1..b45c6082 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -19,6 +19,28 @@ function PengaduanMasyarakat() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + state.pengaduanMasyarakat.create.form.name?.trim() !== '' && + state.pengaduanMasyarakat.create.form.email?.trim() !== '' && + state.pengaduanMasyarakat.create.form.nomorTelepon?.trim() !== '' && + state.pengaduanMasyarakat.create.form.nik?.trim() !== '' && + state.pengaduanMasyarakat.create.form.judulPengaduan?.trim() !== '' && + state.pengaduanMasyarakat.create.form.lokasiKejadian?.trim() !== '' && + !isHtmlEmpty(state.pengaduanMasyarakat.create.form.deskripsiPengaduan) && + state.pengaduanMasyarakat.create.form.jenisPengaduanId?.trim() !== '' && + file !== null + ); + }; + useEffect(() => { // ✅ Panggil load data jenis layanan dari backend if (!state.jenisPengaduan.findMany.data) { @@ -207,7 +229,11 @@ function PengaduanMasyarakat() { - diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx index 42a22ec3..de174975 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx @@ -37,6 +37,24 @@ function Page() { }; }; + // Check if form is valid + const isFormValid = () => { + return ( + beasiswaDesa.create.form.namaLengkap?.trim() !== '' && + beasiswaDesa.create.form.nis?.trim() !== '' && + beasiswaDesa.create.form.kelas?.trim() !== '' && + beasiswaDesa.create.form.jenisKelamin?.trim() !== '' && + beasiswaDesa.create.form.alamatDomisili?.trim() !== '' && + beasiswaDesa.create.form.tempatLahir?.trim() !== '' && + beasiswaDesa.create.form.tanggalLahir?.trim() !== '' && + beasiswaDesa.create.form.namaOrtu?.trim() !== '' && + beasiswaDesa.create.form.nik?.trim() !== '' && + beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' && + beasiswaDesa.create.form.penghasilan?.trim() !== '' && + beasiswaDesa.create.form.noHp?.trim() !== '' + ); + }; + const { data, page, totalPages, loading, load } = ungggulanDesa.findMany; useShallowEffect(() => { @@ -238,7 +256,7 @@ function Page() { onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} /> - + diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx index c408b5b1..6678c274 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx @@ -46,6 +46,24 @@ export default function BeasiswaPage() { }; }; + // Check if form is valid + const isFormValid = () => { + return ( + beasiswaDesa.create.form.namaLengkap?.trim() !== '' && + beasiswaDesa.create.form.nis?.trim() !== '' && + beasiswaDesa.create.form.kelas?.trim() !== '' && + beasiswaDesa.create.form.jenisKelamin?.trim() !== '' && + beasiswaDesa.create.form.alamatDomisili?.trim() !== '' && + beasiswaDesa.create.form.tempatLahir?.trim() !== '' && + beasiswaDesa.create.form.tanggalLahir?.trim() !== '' && + beasiswaDesa.create.form.namaOrtu?.trim() !== '' && + beasiswaDesa.create.form.nik?.trim() !== '' && + beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' && + beasiswaDesa.create.form.penghasilan?.trim() !== '' && + beasiswaDesa.create.form.noHp?.trim() !== '' + ); + }; + const handleSubmit = async () => { await beasiswaDesa.create.create(); resetForm(); @@ -391,6 +409,7 @@ export default function BeasiswaPage() { radius="xl" bg={colors['blue-button']} onClick={handleSubmit} + disabled={!isFormValid()} style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }} > Kirim diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx index e137e07a..368d19ca 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx @@ -42,6 +42,24 @@ export default function ModalPeminjaman({ const BATAS_HARI_PINJAM = 4; + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + snap.create.form.nama?.trim() !== '' && + snap.create.form.noTelp?.trim() !== '' && + snap.create.form.alamat?.trim() !== '' && + snap.create.form.tanggalPinjam?.trim() !== '' && + !isHtmlEmpty(snap.create.form.catatan) + ); + }; + // Reset form setiap modal dibuka useEffect(() => { if (opened && buku) { @@ -222,13 +240,13 @@ export default function ModalPeminjaman({ + + + +// Divider dengan variant + {/* Default */} + + +``` + +#### C. Page Header Component + +```tsx +import { UnifiedPageHeader } from '@/components/admin/UnifiedTypography'; + + + Tambah Baru + + } +/> +``` + +### 3. **Menggunakan Theme Tokens Langsung** + +```tsx +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; + +function MyComponent() { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( +
+

+ Konten dengan styling konsisten +

+
+ ); +} +``` + +--- + +## 🌓 Dark Mode Toggle + +### Otomatis di Header + +Dark mode toggle sudah terintegrasi di header admin dashboard. User bisa toggle dengan klik tombol 🌙/☀️. + +### Manual Toggle + +```tsx +import { useDarkMode } from '@/state/darkModeStore'; +import { DarkModeToggle } from '@/components/admin/DarkModeToggle'; + +function MyComponent() { + const { isDark, toggle } = useDarkMode(); + + return ( +
+

Current mode: {isDark ? 'Dark' : 'Light'}

+ + {/* Gunakan component toggle */} + + + {/* Atau manual */} + +
+ ); +} +``` + +### Persistensi + +Dark mode preference disimpan di `localStorage` dengan key `darmasaba-admin-dark-mode`. +Preference akan tetap ada saat user refresh halaman atau kembali nanti. + +--- + +## 📝 Contoh Penggunaan Lengkap + +### Contoh 1: List Page dengan Table + +```tsx +'use client' +import { UnifiedPageHeader, UnifiedText } from '@/components/admin/UnifiedTypography'; +import UnifiedCard from '@/components/admin/UnifiedSurface'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { Button, Table, TableTr, TableTh, TableTd } from '@mantine/core'; + +export default function DaftarBerita() { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( +
+ {/* Header Halaman */} + + + Tambah Berita + + } + /> + + {/* Card untuk Table */} + + + + + + Judul + + + Kategori + + + + + {data.map((item) => ( + + + {item.judul} + + + + {item.kategori} + + + + ))} + +
+
+
+ ); +} +``` + +### Contoh 2: Detail Page + +```tsx +import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography'; +import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface'; + +export default function DetailBerita({ data }) { + return ( + + + {data.judul} + + + + + Kategori + {data.kategori} + + + + + + Deskripsi + {data.deskripsi} + + + + + + Konten +
+ + + + + + + + + + + ); +} +``` + +--- + +## 🎨 Mengedit Style + +### Edit Warna Dark Mode + +File: `src/utils/themeTokens.ts` + +```typescript +const darkColors = { + // Background Layers + bgBase: '#0B1220', // ← Edit di sini + bgApp: '#0F172A', + bgCard: '#162235', + bgSurface: '#1E2A3D', + + // Text + textPrimary: '#E5E7EB', // ← Edit di sini + textSecondary: '#9CA3AF', + + // Accent + primaryAction: '#3B82F6', // ← Edit primary color +}; +``` + +### Edit Warna Light Mode + +```typescript +const lightColors = { + bgBase: '#f6f9fc', + bgCard: '#ffffff', + textPrimary: '#1a1b1e', + primaryAction: baseColors['blue-button'], // Dari colors.ts +}; +``` + +### Edit Typography + +```typescript +typography: { + h1: { + fz: '2rem', // ← Edit ukuran + fw: 700, // ← Edit weight + lh: 1.2, // ← Edit line height + }, + body: { + fz: '1rem', + fw: 400, + lh: 1.5, + }, +} +``` + +### Edit Spacing & Radius + +```typescript +spacing: { + xs: '0.625rem', // 10px + sm: '1rem', // 16px + md: '1.5rem', // 24px + lg: '2rem', // 32px +} + +radius: { + sm: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px +} +``` + +--- + +## ✅ Checklist Migrasi + +Komponen yang sudah diupdate dengan dark mode: + +- ✅ `src/app/admin/layout.tsx` +- ✅ `src/app/admin/(dashboard)/_com/header.tsx` +- ✅ `src/app/admin/(dashboard)/_com/judulList.tsx` +- ✅ `src/app/admin/(dashboard)/_com/judulListTab.tsx` +- ✅ `src/components/admin/UnifiedTypography.tsx` +- ✅ `src/components/admin/UnifiedSurface.tsx` +- ✅ `src/components/admin/DarkModeToggle.tsx` +- ✅ `src/utils/themeTokens.ts` + +Komponen yang perlu diupdate (TODO): + +- [ ] Komponen di `src/app/admin/(dashboard)/desa/` +- [ ] Komponen di `src/app/admin/(dashboard)/ppid/` +- [ ] Komponen di `src/app/admin/(dashboard)/kesehatan/` +- [ ] Komponen di `src/app/admin/(dashboard)/pendidikan/` +- [ ] Komponen di `src/app/admin/(dashboard)/ekonomi/` +- [ ] Dan lain-lain... + +--- + +## 📚 Referensi + +- [Dark Mode Specification](../../../darkMode.md) - Spesifikasi lengkap dark mode +- [Mantine Theme System](https://mantine.dev/theming/theme-object/) +- [Mantine Dark Mode](https://mantine.dev/theming/dark-mode/) +- [Valtio State Management](https://github.com/pmndrs/valtio) + +--- + +## 💡 Tips + +1. **Selalu gunakan unified components** untuk konsistensi dark/light mode +2. **Edit di `themeTokens.ts`** untuk perubahan global +3. **Test dark mode** setelah perubahan style +4. **Gunakan color semantic** (`primary`, `secondary`, `muted`) bukan hex langsung +5. **Jangan hardcode shadow** di dark mode (spec: "Jangan pakai shadow hitam") +6. **Border harus terlihat** di dark mode (opacity > 20%) + +--- + +## 🆘 Troubleshooting + +### Style tidak berubah setelah edit themeTokens.ts? + +1. Clear browser cache (Cmd+Shift+R / Ctrl+Shift+R) +2. Restart dev server: `bun run dev` +3. Pastikan komponen menggunakan unified components + +### Dark mode tidak berfungsi? + +1. Cek `darkModeStore.ts` sudah diimport +2. Pastikan `useDarkMode()` hook digunakan +3. Clear localStorage: `localStorage.clear()` +4. Cek console untuk error + +### Border tidak terlihat di dark mode? + +Pastikan menggunakan `tokens.colors.border.default` atau `tokens.colors.border.soft`, bukan hardcode warna. + +### Component tidak re-render? + +1. Pastikan `'use client'` ada di file component +2. Gunakan `useSnapshot()` jika menggunakan Valtio di non-event handler +3. Cek console untuk error + +--- + +## 📐 Spesifikasi Dark Mode + +Untuk spesifikasi lengkap dark mode (layout rules, table styles, button rules, dll), lihat: +**[`darkMode.md`](../../../darkMode.md)** + +Highlights: +- ✅ Background layers berbeda (base, app, card, surface) +- ✅ Border wajib terlihat (tidak flat) +- ✅ Active state dengan accent bar (2-3px) +- ✅ Tidak pakai shadow hitam +- ✅ Hover state dengan background soft +- ✅ Text kontras terbaca + +--- + +**Last Updated:** February 20, 2026 +**Version:** 2.0.0 (Dark Mode Ready) +**Based on:** darkMode.md specification diff --git a/src/components/admin/UnifiedSurface.tsx b/src/components/admin/UnifiedSurface.tsx new file mode 100644 index 00000000..c00f911b --- /dev/null +++ b/src/components/admin/UnifiedSurface.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { Paper, Box, BoxProps, Divider, DividerProps } from '@mantine/core'; +import React from 'react'; + +/** + * Unified Surface Components + * + * Komponen container/card dengan styling konsisten + * Mendukung dark mode sesuai spesifikasi darkMode.md + * + * Usage: + * import { UnifiedCard, UnifiedDivider } from '@/components/admin/UnifiedSurface'; + * + * + * Title + * Content + * + */ + +// ============================================================================ +// Unified Card Component + * ============================================================================ + +interface UnifiedCardProps extends BoxProps { + withBorder?: boolean; + shadow?: 'none' | 'sm' | 'md' | 'lg'; + padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + hoverable?: boolean; + children: React.ReactNode; +} + +export function UnifiedCard({ + withBorder = true, + shadow = 'none', // Sesuai spec: Jangan pakai shadow hitam + padding = 'md', + hoverable = false, + children, + style, + ...props +}: UnifiedCardProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + case 'xl': + return tokens.spacing.xl; + default: + return tokens.spacing.md; + } + }; + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Card Section Components +// ============================================================================ + +interface UnifiedCardSectionProps { + children: React.ReactNode; + padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg'; + border?: 'none' | 'top' | 'bottom'; + style?: React.CSSProperties; +} + +UnifiedCard.Header = function UnifiedCardHeader({ + children, + padding = 'md', + border = 'bottom', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + + return ( + + {children} + + ); +}; + +UnifiedCard.Body = function UnifiedCardBody({ + children, + padding = 'md', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + return ( + + {children} + + ); +}; + +UnifiedCard.Footer = function UnifiedCardFooter({ + children, + padding = 'md', + border = 'top', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + + return ( + + {children} + + ); +}; + +// ============================================================================ +// Unified Divider Component +// ============================================================================ + +interface UnifiedDividerProps extends DividerProps { + variant?: 'default' | 'soft' | 'strong'; +} + +export function UnifiedDivider({ + variant = 'soft', // Default soft sesuai spec + my = 'md', + ...props +}: UnifiedDividerProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getColor = () => { + switch (variant) { + case 'default': + return tokens.colors.border.default; + case 'soft': + return tokens.colors.border.soft; + case 'strong': + return tokens.colors.border.strong; + default: + return tokens.colors.border.soft; + } + }; + + return ; +} + +export default UnifiedCard; diff --git a/src/components/admin/UnifiedTypography.tsx b/src/components/admin/UnifiedTypography.tsx new file mode 100644 index 00000000..565be609 --- /dev/null +++ b/src/components/admin/UnifiedTypography.tsx @@ -0,0 +1,268 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens, getResponsiveFz } from '@/utils/themeTokens'; +import { Text, Title, Box, BoxProps } from '@mantine/core'; +import React from 'react'; + +/** + * Unified Typography Components + * + * Komponen text dengan styling konsisten di seluruh aplikasi + * Mendukung dark mode sesuai spesifikasi darkMode.md + * + * Usage: + * import { UnifiedText, UnifiedTitle } from '@/components/admin/UnifiedTypography'; + * + * Judul Halaman + * Konten teks + */ + +// ============================================================================ +// Unified Title Component +// ============================================================================ + +interface UnifiedTitleProps { + order?: 1 | 2 | 3 | 4 | 5 | 6; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: 'primary' | 'secondary' | 'brand' | string; + mb?: string; + mt?: string; + ml?: string; + mr?: string; + mx?: string; + my?: string; + style?: React.CSSProperties; +} + +export function UnifiedTitle({ + order = 1, + children, + align = 'left', + color = 'primary', + mb, + mt, + ml, + mr, + mx, + my, + style, +}: UnifiedTitleProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + const responsiveFz = getResponsiveFz(isDark); + + const getTypography = () => { + switch (order) { + case 1: + return tokens.typography.h1; + case 2: + return tokens.typography.h2; + case 3: + return tokens.typography.h3; + case 4: + return tokens.typography.h4; + default: + return tokens.typography.body; + } + }; + + const typo = getTypography(); + + const getColor = () => { + if (color === 'primary') return tokens.colors.text.primary; + if (color === 'secondary') return tokens.colors.text.secondary; + if (color === 'brand') return tokens.colors.brand; + return color; + }; + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Text Component +// ============================================================================ + +interface UnifiedTextProps { + size?: 'small' | 'body' | 'label'; + weight?: 'normal' | 'medium' | 'bold'; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: 'primary' | 'secondary' | 'tertiary' | 'muted' | 'brand' | 'link' | string; + lineClamp?: number; + truncate?: 'start' | 'end' | 'middle' | boolean; + span?: boolean; + style?: React.CSSProperties; +} + +export function UnifiedText({ + size = 'body', + weight = 'normal', + children, + align = 'left', + color = 'primary', + lineClamp, + truncate, + span = false, + style, +}: UnifiedTextProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getTypography = () => { + switch (size) { + case 'small': + return tokens.typography.small; + case 'label': + return tokens.typography.label; + default: + return tokens.typography.body; + } + }; + + const getWeight = () => { + switch (weight) { + case 'normal': + return 400; + case 'medium': + return 500; + case 'bold': + return 700; + default: + return 400; + } + }; + + const getColor = () => { + switch (color) { + case 'primary': + return tokens.colors.text.primary; + case 'secondary': + return tokens.colors.text.secondary; + case 'tertiary': + return tokens.colors.text.tertiary; + case 'muted': + return tokens.colors.text.muted; + case 'brand': + return tokens.colors.brand; + case 'link': + return tokens.colors.text.link; + default: + return color; + } + }; + + const typo = getTypography(); + const fw = getWeight(); + const textColor = getColor(); + + if (span) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Page Header Component +// +// Header standar untuk setiap halaman admin +// Sesuai spesifikasi: Section Header dengan font weight lebih besar +// ============================================================================ + +interface UnifiedPageHeaderProps extends BoxProps { + title: string; + subtitle?: string; + action?: React.ReactNode; + showBorder?: boolean; +} + +export function UnifiedPageHeader({ + title, + subtitle, + action, + showBorder = true, + style, + ...props +}: UnifiedPageHeaderProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( + +
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {action &&
{action}
} +
+
+ ); +} + +export default UnifiedText; diff --git a/src/state/darkModeStore.ts b/src/state/darkModeStore.ts new file mode 100644 index 00000000..25032410 --- /dev/null +++ b/src/state/darkModeStore.ts @@ -0,0 +1,76 @@ +/** + * 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 system preference +const getInitialDarkMode = (): boolean => { + if (typeof window === 'undefined') return false; + + const stored = localStorage.getItem(STORAGE_KEY); + if (stored !== null) { + return stored === 'true'; + } + + // Fallback to system preference + return window.matchMedia('(prefers-color-scheme: dark)').matches; +}; + +class DarkModeStore { + public isDark: boolean; + + constructor() { + this.isDark = getInitialDarkMode(); + } + + public toggle() { + this.isDark = !this.isDark; + this.persist(); + } + + public setDarkMode(value: boolean) { + this.isDark = value; + this.persist(); + } + + private persist() { + if (typeof window !== 'undefined') { + localStorage.setItem(STORAGE_KEY, String(this.isDark)); + } + } +} + +// Create proxy instance +const store = new DarkModeStore(); + +export const darkModeStore = proxy(store); + +// Hook untuk menggunakan dark mode state di React components +export const useDarkMode = () => { + const snapshot = useSnapshot(darkModeStore); + return { + isDark: snapshot.isDark, + toggle: () => darkModeStore.toggle(), + setDarkMode: (value: boolean) => darkModeStore.setDarkMode(value), + }; +}; + +export default darkModeStore; diff --git a/src/styles/dark-mode-table.css b/src/styles/dark-mode-table.css new file mode 100644 index 00000000..ecc08032 --- /dev/null +++ b/src/styles/dark-mode-table.css @@ -0,0 +1,31 @@ +/** + * Dark Mode Table Styles + * + * Override Mantine table hover styles untuk dark mode + * Agar teks putih tetap terlihat saat hover + */ + +/* Dark mode table hover */ +[data-mantine-color-scheme="dark"] { + /* Table hover */ + .mantine-Table-tr:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + /* Table striped hover */ + .mantine-Table-striped .mantine-Table-tr:nth-of-type(odd):hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + /* Table with column borders */ + .mantine-Table-withColumnBorders .mantine-Table-tr:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } +} + +/* Light mode table hover - default Mantine behavior */ +[data-mantine-color-scheme="light"] { + .mantine-Table-tr:hover { + background-color: rgba(0, 0, 0, 0.02) !important; + } +} diff --git a/src/utils/themeTokens.ts b/src/utils/themeTokens.ts new file mode 100644 index 00000000..75c133a0 --- /dev/null +++ b/src/utils/themeTokens.ts @@ -0,0 +1,383 @@ +/** + * Unified Theme Tokens for Admin Dashboard + * + * Berdasarkan spesifikasi: darkMode.md + * + * Semua styling constants disimpan di sini untuk konsistensi + * Edit di sini = edit di seluruh aplikasi + * + * Usage: + * import { themeTokens } from '@/utils/themeTokens'; + * + * // Light mode (default) + * const tokens = themeTokens(false); + * + * // Dark mode + * const tokens = themeTokens(true); + */ + +export type ThemeTokens = { + // Colors + colors: { + primary: string; + primaryLight: string; + primaryDark: string; + gradient: { + from: string; + to: string; + }; + // Backgrounds + bg: { + base: string; + main: string; + app: string; + surface: string; + surfaceElevated: string; + header: string; + navbar: string; + card: string; + hover: string; + tableHeader: string; + tableHover: string; + }; + // Text + text: { + primary: string; + secondary: string; + tertiary: string; + muted: string; + brand: string; + inverse: string; + link: string; + }; + // Borders + border: { + default: string; + soft: string; + strong: string; + }; + // Status + success: string; + warning: string; + error: string; + info: string; + }; + + // Typography + typography: { + h1: { + fz: string; + fw: number; + lh: number; + }; + h2: { + fz: string; + fw: number; + lh: number; + }; + h3: { + fz: string; + fw: number; + lh: number; + }; + h4: { + fz: string; + fw: number; + lh: number; + }; + body: { + fz: string; + fw: number; + lh: number; + }; + small: { + fz: string; + fw: number; + lh: number; + }; + label: { + fz: string; + fw: number; + lh: number; + }; + }; + + // Spacing + spacing: { + xs: string; + sm: string; + md: string; + lg: string; + xl: string; + }; + + // Border Radius + radius: { + sm: string; + md: string; + lg: string; + xl: string; + }; + + // Shadows + shadows: { + none: string; + sm: string; + md: string; + lg: string; + }; + + // Layout + layout: { + headerHeight: number; + navbarWidth: { + base: number; + sm: number; + lg: number; + }; + }; +}; + +export const themeTokens = (isDark: boolean = false): ThemeTokens => { + // Base colors - tetap menggunakan colors.ts sebagai base untuk light mode + const baseColors = { + 'orange': '#FCAE00', + 'blue-button': '#0A4E78', + 'blue-button-1': '#E5F2FA', + 'blue-button-2': '#B8DAEF', + 'blue-button-3': '#8AC1E3', + 'blue-button-4': '#5DA9D8', + 'blue-button-5': '#2F91CC', + 'blue-button-6': '#083F61', + 'blue-button-7': '#062F49', + 'blue-button-8': '#041F32', + 'blue-button-trans': '#628EC6', + 'white-1': '#FBFBFC', + 'white-trans-1': 'rgba(255, 255, 255, 0.5)', + 'white-trans-2': 'rgba(255, 255, 255, 0.7)', + 'white-trans-3': 'rgba(255, 255, 255, 0.9)', + 'grey-1': '#F4F5F6', + 'grey-2': '#CBCACD', + 'Bg': '#D1d9e8', + 'BG-trans': '#B1C5F2', + }; + + /** + * DARK MODE PALETTE + * Berdasarkan spesifikasi: darkMode.md + */ + const darkColors = { + // Background Layers + bgBase: '#0B1220', + bgApp: '#0F172A', + bgCard: '#162235', + bgSurface: '#1E2A3D', + + // Borders + borderDefault: '#2A3A52', + borderSoft: '#22314A', + + // Text + textPrimary: '#E5E7EB', + textSecondary: '#9CA3AF', + textMuted: '#6B7280', + textInverse: '#020617', + + // Accent & Actions + primaryAction: '#3B82F6', + primaryHover: '#2563EB', + primaryActive: '#1D4ED8', + link: '#60A5FA', + + // Status + success: '#22C55E', + warning: '#FACC15', + error: '#EF4444', + info: '#38BDF8', + + // Hover states + hoverSoft: 'rgba(255,255,255,0.03)', + hoverMedium: 'rgba(255,255,255,0.04)', + activeAccent: 'rgba(59,130,246,0.15)', + }; + + /** + * LIGHT MODE PALETTE + * Original light theme + */ + const lightColors = { + bgBase: '#f6f9fc', + bgApp: '#ffffff', + bgCard: '#ffffff', + bgSurface: '#f8fafc', + borderDefault: '#e2e8f0', + borderSoft: '#e9ecef', + textPrimary: '#1a1b1e', + textSecondary: '#495057', + textMuted: '#868e96', + textInverse: '#ffffff', + primaryAction: baseColors['blue-button'], + primaryHover: '#083F61', + primaryActive: '#062F49', + link: '#2563eb', + hoverSoft: 'rgba(25, 113, 194, 0.03)', + hoverMedium: 'rgba(25, 113, 194, 0.05)', + activeAccent: 'rgba(25, 113, 194, 0.1)', + }; + + const current = isDark ? darkColors : lightColors; + + return { + colors: { + primary: current.primaryAction, + primaryLight: isDark ? current.activeAccent : baseColors['blue-button-1'], + primaryDark: current.primaryActive, + gradient: { + from: current.primaryAction, + to: isDark ? '#60A5FA' : '#228be6', + }, + bg: { + base: current.bgBase, + main: isDark ? current.bgBase : 'linear-gradient(180deg, #fdfdfd, #f6f9fc)', + app: current.bgApp, + surface: current.bgSurface, + surfaceElevated: isDark ? '#253347' : '#ffffff', + header: isDark + ? `linear-gradient(180deg, ${current.bgApp} 0%, ${current.bgBase} 100%)` + : 'linear-gradient(90deg, #ffffff, #f9fbff)', + navbar: current.bgApp, + card: current.bgCard, + hover: current.hoverMedium, + tableHeader: current.bgSurface, + tableHover: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)', + }, + text: { + primary: current.textPrimary, + secondary: current.textSecondary, + tertiary: current.textMuted, + muted: current.textMuted, + brand: current.primaryAction, + inverse: current.textInverse, + link: current.link, + }, + border: { + default: current.borderDefault, + soft: current.borderSoft, + strong: isDark ? '#3A4A62' : '#ced4da', + }, + success: current.success, + warning: current.warning, + error: current.error, + info: current.info, + }, + + typography: { + h1: { + fz: isDark ? '2rem' : '2.25rem', + fw: 700, + lh: 1.2, + }, + h2: { + fz: isDark ? '1.75rem' : '2rem', + fw: 700, + lh: 1.25, + }, + h3: { + fz: isDark ? '1.5rem' : '1.75rem', + fw: 700, + lh: 1.3, + }, + h4: { + fz: isDark ? '1.25rem' : '1.5rem', + fw: 600, + lh: 1.35, + }, + body: { + fz: '1rem', + fw: 400, + lh: 1.5, + }, + small: { + fz: '0.875rem', + fw: 400, + lh: 1.4, + }, + label: { + fz: '0.75rem', + fw: 600, + lh: 1.4, + }, + }, + + spacing: { + xs: '0.625rem', + sm: '1rem', + md: '1.5rem', + lg: '2rem', + xl: '2.5rem', + }, + + radius: { + sm: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px + xl: '1.25rem', // 20px + }, + + shadows: { + none: 'none', + sm: isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)', + md: isDark ? '0 4px 6px rgba(0,0,0,0.3)' : '0 4px 6px rgba(0,0,0,0.1)', + lg: isDark ? '0 10px 15px rgba(0,0,0,0.3)' : '0 10px 15px rgba(0,0,0,0.1)', + }, + + layout: { + headerHeight: 64, + navbarWidth: { + base: 260, + sm: 280, + lg: 300, + }, + }, + }; +}; + +// Export default theme instances +export const lightTheme = themeTokens(false); +export const darkTheme = themeTokens(true); + +// Helper untuk mendapatkan responsive font size +export const getResponsiveFz = (isDark: boolean = false) => ({ + base: isDark ? 'md' : 'lg', + md: isDark ? 'lg' : 'xl', +}); + +// Helper untuk mendapatkan color berdasarkan state +export const getActiveColor = (isActive: boolean, isDark: boolean = false) => + isActive ? themeTokens(isDark).colors.primary : isDark ? themeTokens(isDark).colors.text.secondary : 'gray'; + +// Helper untuk mendapatkan background hover +export const getHoverBackground = (isActive: boolean, isDark: boolean = false) => { + const tokens = themeTokens(isDark); + return isActive + ? tokens.colors.bg.hover + : tokens.colors.bg.hover; +}; + +// Helper untuk active state dengan accent bar (sidebar) +export const getActiveStateStyles = (isActive: boolean, isDark: boolean = false) => { + const tokens = themeTokens(isDark); + + if (isActive) { + return { + backgroundColor: isDark ? tokens.colors.bg.hover : 'rgba(25, 113, 194, 0.1)', + borderLeft: isDark ? `3px solid ${tokens.colors.primary}` : '3px solid #1971c2', + }; + } + + return { + '&:hover': { + backgroundColor: tokens.colors.bg.hover, + }, + }; +};