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:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -138,3 +138,19 @@
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* ─── Modal animations ───────────────────────────────── */
|
||||
@keyframes modalBackdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes modalEnter {
|
||||
from { opacity: 0; transform: scale(0.95) translateY(8px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
.animate-modal-backdrop {
|
||||
animation: modalBackdropIn 0.15s ease-out;
|
||||
}
|
||||
.animate-modal-enter {
|
||||
animation: modalEnter 0.2s ease-out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user