feat: replace native confirm() dialogs with custom modal component

- Add ModalProvider context and useConfirm hook (confirm-modal.tsx)
- Support danger/warning/info variants with icons and colors
- Animated backdrop and dialog with CSS keyframes
- Accessible: aria-modal, role=dialog, ESC to close, auto-focus
- Replace all 6 native confirm() calls across dashboard pages
- Integrate ModalProvider in app providers
This commit is contained in:
keyhan
2026-04-07 23:38:55 +03:30
parent 5979b48a61
commit ac5278d73b
10 changed files with 222 additions and 14 deletions
@@ -7,6 +7,7 @@ import api from '@/lib/api';
import { toast } from 'react-toastify';
import type { Application } from '@/types';
import { Search, X, Package, Hexagon, User, Database, Box } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
const statusColors: Record<string, string> = {
running: 'badge-green',
@@ -20,6 +21,7 @@ const statusColors: Record<string, string> = {
export default function AdminAppsPage() {
const queryClient = useQueryClient();
const confirm = useConfirm();
const [search, setSearch] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
@@ -227,7 +229,10 @@ export default function AdminAppsPage() {
View
</Link>
<button
onClick={() => { if (confirm('Delete this application?')) deleteMutation.mutate(app.id); }}
onClick={async () => {
const ok = await confirm({ title: 'Delete Application', message: `Are you sure you want to delete "${app.name}"?`, confirmText: 'Delete', variant: 'danger' });
if (ok) deleteMutation.mutate(app.id);
}}
className="text-xs px-3 py-1.5 rounded-lg text-red-600 hover:bg-red-50 transition-colors"
>
Delete
@@ -6,6 +6,7 @@ import api from '@/lib/api';
import { toast } from 'react-toastify';
import type { ServicePlan, BillingCycle, PricingResourceType } from '@/types';
import { DollarSign, Plus, Trash2, Edit2, ToggleLeft, ToggleRight, ChevronDown, ChevronUp } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
const runtimeOptions = [
{ value: 'nodejs', label: 'Node.js' },
@@ -47,6 +48,7 @@ const emptyRule = (): RuleForm => ({ resourceType: 'base_fee', unitPrice: '', de
export default function AdminBillingPage() {
const queryClient = useQueryClient();
const confirm = useConfirm();
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [expandedPlan, setExpandedPlan] = useState<string | null>(null);
@@ -282,7 +284,10 @@ export default function AdminBillingPage() {
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => { if (confirm('Delete this plan?')) deleteMutation.mutate(plan.id); }}
onClick={async () => {
const ok = await confirm({ title: 'Delete Plan', message: `Are you sure you want to delete "${plan.name}"?`, confirmText: 'Delete', variant: 'danger' });
if (ok) deleteMutation.mutate(plan.id);
}}
className="p-1 text-red-500 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
@@ -6,6 +6,7 @@ import api from '@/lib/api';
import { toast } from 'react-toastify';
import type { Cluster, ClusterResources } from '@/types';
import { Server, CheckCircle, XCircle, BarChart3, Clock, RotateCw, Plug, X } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
function ResourcePanel({ clusterId }: { clusterId: string }) {
const { data, isLoading, error } = useQuery<ClusterResources>({
@@ -121,6 +122,7 @@ function ResourcePanel({ clusterId }: { clusterId: string }) {
export default function AdminClustersPage() {
const queryClient = useQueryClient();
const confirm = useConfirm();
const [showForm, setShowForm] = useState(false);
const [testingId, setTestingId] = useState<string | null>(null);
const [expandedResources, setExpandedResources] = useState<Set<string>>(new Set());
@@ -333,7 +335,10 @@ export default function AdminClustersPage() {
{testingId === cluster.id ? <><Clock className="w-3 h-3 inline animate-spin" /> Testing...</> : <><Plug className="w-3 h-3 inline" /> Test</>}
</button>
<button
onClick={() => { if (confirm('Remove this cluster?')) deleteMutation.mutate(cluster.id); }}
onClick={async () => {
const ok = await confirm({ title: 'Remove Cluster', message: `Are you sure you want to remove "${cluster.name}"?`, confirmText: 'Remove', variant: 'danger' });
if (ok) deleteMutation.mutate(cluster.id);
}}
className="text-sm text-red-600 hover:text-red-800 font-medium"
>
Remove
@@ -6,9 +6,11 @@ import api from '@/lib/api';
import { toast } from 'react-toastify';
import type { Cluster, ClusterPool } from '@/types';
import { Scale, BarChart3, RotateCw, CheckCircle, XCircle, Pencil, Clock, X } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
export default function AdminPoolsPage() {
const queryClient = useQueryClient();
const confirm = useConfirm();
const [showForm, setShowForm] = useState(false);
const [editingPool, setEditingPool] = useState<ClusterPool | null>(null);
const [form, setForm] = useState({
@@ -318,10 +320,14 @@ export default function AdminPoolsPage() {
<Pencil className="w-3 h-3 inline" /> Edit
</button>
<button
onClick={() => {
if (confirm(`Delete pool "${pool.name}"? Apps already assigned to this pool will keep their current cluster.`)) {
deleteMutation.mutate(pool.id);
}
onClick={async () => {
const ok = await confirm({
title: `Delete Pool "${pool.name}"`,
message: 'Apps already assigned to this pool will keep their current cluster.',
confirmText: 'Delete',
variant: 'danger',
});
if (ok) deleteMutation.mutate(pool.id);
}}
className="text-sm text-red-600 hover:text-red-800"
>
+10 -4
View File
@@ -7,6 +7,7 @@ import { toast } from 'react-toastify';
import type { Application, Deployment, ResourceUsage, ClusterPublic, ClusterPoolPublic } from '@/types';
import { useState, useRef, useCallback, useEffect } from 'react';
import { Hexagon, Rocket, Play, Square, RotateCw, Globe, Package, Server, Scale, CheckCircle, Clock, XCircle, AlertCircle, Link, GitBranch, KeyRound, FolderUp, BarChart3, ChevronDown, FileText, Monitor, Hammer, Settings, RefreshCw, Upload, Circle, Pin, Database, Eye, EyeOff, Copy, Check } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
const statusColors: Record<string, string> = {
running: 'badge-green',
@@ -46,6 +47,7 @@ export default function AppDetailPage() {
const params = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const confirm = useConfirm();
const appId = params.id as string;
const [showLogs, setShowLogs] = useState(false);
const [logTab, setLogTab] = useState<'pod' | 'build'>('pod');
@@ -378,10 +380,14 @@ export default function AppDetailPage() {
const isRunning = latestStatus === 'running';
const isInProgress = latestStatus === 'building' || latestStatus === 'deploying' || latestStatus === 'pending';
const handleDelete = () => {
if (confirm(`Are you sure you want to delete "${app.name}"?\n\nThis will permanently remove:\n• All Kubernetes resources (pods, services, ingress)\n• Database and volumes\n• All deployment records\n• Uploaded source code`)) {
deleteMutation.mutate();
}
const handleDelete = async () => {
const ok = await confirm({
title: `Delete "${app.name}"?`,
message: 'This will permanently remove:\n• All Kubernetes resources (pods, services, ingress)\n• Database and volumes\n• All deployment records\n• Uploaded source code',
confirmText: 'Delete',
variant: 'danger',
});
if (ok) deleteMutation.mutate();
};
return (
+6 -1
View File
@@ -6,6 +6,7 @@ import api from '@/lib/api';
import { toast } from 'react-toastify';
import type { Application } from '@/types';
import { Rocket, Package, Hexagon, Database, Box } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
const statusColors: Record<string, string> = {
running: 'badge-green',
@@ -19,6 +20,7 @@ const statusColors: Record<string, string> = {
export default function AppsPage() {
const queryClient = useQueryClient();
const confirm = useConfirm();
const { data: apps = [], isLoading } = useQuery<Application[]>({
queryKey: ['applications'],
@@ -122,7 +124,10 @@ export default function AppsPage() {
View
</Link>
<button
onClick={() => { if (confirm('Delete this application?')) deleteMutation.mutate(app.id); }}
onClick={async () => {
const ok = await confirm({ title: 'Delete Application', message: `Are you sure you want to delete "${app.name}"?`, confirmText: 'Delete', variant: 'danger' });
if (ok) deleteMutation.mutate(app.id);
}}
className="text-xs px-3 py-1.5 rounded-lg text-red-600 hover:bg-red-50 transition-colors"
>
Delete