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 { toast } from 'react-toastify';
|
||||||
import type { Application } from '@/types';
|
import type { Application } from '@/types';
|
||||||
import { Search, X, Package, Hexagon, User, Database, Box } from 'lucide-react';
|
import { Search, X, Package, Hexagon, User, Database, Box } from 'lucide-react';
|
||||||
|
import { useConfirm } from '@/components/confirm-modal';
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
running: 'badge-green',
|
running: 'badge-green',
|
||||||
@@ -20,6 +21,7 @@ const statusColors: Record<string, string> = {
|
|||||||
|
|
||||||
export default function AdminAppsPage() {
|
export default function AdminAppsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||||
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
|
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
|
||||||
@@ -227,7 +229,10 @@ export default function AdminAppsPage() {
|
|||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<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"
|
className="text-xs px-3 py-1.5 rounded-lg text-red-600 hover:bg-red-50 transition-colors"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import api from '@/lib/api';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import type { ServicePlan, BillingCycle, PricingResourceType } from '@/types';
|
import type { ServicePlan, BillingCycle, PricingResourceType } from '@/types';
|
||||||
import { DollarSign, Plus, Trash2, Edit2, ToggleLeft, ToggleRight, ChevronDown, ChevronUp } from 'lucide-react';
|
import { DollarSign, Plus, Trash2, Edit2, ToggleLeft, ToggleRight, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { useConfirm } from '@/components/confirm-modal';
|
||||||
|
|
||||||
const runtimeOptions = [
|
const runtimeOptions = [
|
||||||
{ value: 'nodejs', label: 'Node.js' },
|
{ value: 'nodejs', label: 'Node.js' },
|
||||||
@@ -47,6 +48,7 @@ const emptyRule = (): RuleForm => ({ resourceType: 'base_fee', unitPrice: '', de
|
|||||||
|
|
||||||
export default function AdminBillingPage() {
|
export default function AdminBillingPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [expandedPlan, setExpandedPlan] = 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" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<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"
|
className="p-1 text-red-500 hover:text-red-700"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import api from '@/lib/api';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import type { Cluster, ClusterResources } from '@/types';
|
import type { Cluster, ClusterResources } from '@/types';
|
||||||
import { Server, CheckCircle, XCircle, BarChart3, Clock, RotateCw, Plug, X } from 'lucide-react';
|
import { Server, CheckCircle, XCircle, BarChart3, Clock, RotateCw, Plug, X } from 'lucide-react';
|
||||||
|
import { useConfirm } from '@/components/confirm-modal';
|
||||||
|
|
||||||
function ResourcePanel({ clusterId }: { clusterId: string }) {
|
function ResourcePanel({ clusterId }: { clusterId: string }) {
|
||||||
const { data, isLoading, error } = useQuery<ClusterResources>({
|
const { data, isLoading, error } = useQuery<ClusterResources>({
|
||||||
@@ -121,6 +122,7 @@ function ResourcePanel({ clusterId }: { clusterId: string }) {
|
|||||||
|
|
||||||
export default function AdminClustersPage() {
|
export default function AdminClustersPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [testingId, setTestingId] = useState<string | null>(null);
|
const [testingId, setTestingId] = useState<string | null>(null);
|
||||||
const [expandedResources, setExpandedResources] = useState<Set<string>>(new Set());
|
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</>}
|
{testingId === cluster.id ? <><Clock className="w-3 h-3 inline animate-spin" /> Testing...</> : <><Plug className="w-3 h-3 inline" /> Test</>}
|
||||||
</button>
|
</button>
|
||||||
<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"
|
className="text-sm text-red-600 hover:text-red-800 font-medium"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import api from '@/lib/api';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import type { Cluster, ClusterPool } from '@/types';
|
import type { Cluster, ClusterPool } from '@/types';
|
||||||
import { Scale, BarChart3, RotateCw, CheckCircle, XCircle, Pencil, Clock, X } from 'lucide-react';
|
import { Scale, BarChart3, RotateCw, CheckCircle, XCircle, Pencil, Clock, X } from 'lucide-react';
|
||||||
|
import { useConfirm } from '@/components/confirm-modal';
|
||||||
|
|
||||||
export default function AdminPoolsPage() {
|
export default function AdminPoolsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingPool, setEditingPool] = useState<ClusterPool | null>(null);
|
const [editingPool, setEditingPool] = useState<ClusterPool | null>(null);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
@@ -318,10 +320,14 @@ export default function AdminPoolsPage() {
|
|||||||
<Pencil className="w-3 h-3 inline" /> Edit
|
<Pencil className="w-3 h-3 inline" /> Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (confirm(`Delete pool "${pool.name}"? Apps already assigned to this pool will keep their current cluster.`)) {
|
const ok = await confirm({
|
||||||
deleteMutation.mutate(pool.id);
|
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"
|
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 type { Application, Deployment, ResourceUsage, ClusterPublic, ClusterPoolPublic } from '@/types';
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
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 { 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> = {
|
const statusColors: Record<string, string> = {
|
||||||
running: 'badge-green',
|
running: 'badge-green',
|
||||||
@@ -46,6 +47,7 @@ export default function AppDetailPage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
const appId = params.id as string;
|
const appId = params.id as string;
|
||||||
const [showLogs, setShowLogs] = useState(false);
|
const [showLogs, setShowLogs] = useState(false);
|
||||||
const [logTab, setLogTab] = useState<'pod' | 'build'>('pod');
|
const [logTab, setLogTab] = useState<'pod' | 'build'>('pod');
|
||||||
@@ -378,10 +380,14 @@ export default function AppDetailPage() {
|
|||||||
const isRunning = latestStatus === 'running';
|
const isRunning = latestStatus === 'running';
|
||||||
const isInProgress = latestStatus === 'building' || latestStatus === 'deploying' || latestStatus === 'pending';
|
const isInProgress = latestStatus === 'building' || latestStatus === 'deploying' || latestStatus === 'pending';
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = async () => {
|
||||||
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`)) {
|
const ok = await confirm({
|
||||||
deleteMutation.mutate();
|
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 (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import api from '@/lib/api';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import type { Application } from '@/types';
|
import type { Application } from '@/types';
|
||||||
import { Rocket, Package, Hexagon, Database, Box } from 'lucide-react';
|
import { Rocket, Package, Hexagon, Database, Box } from 'lucide-react';
|
||||||
|
import { useConfirm } from '@/components/confirm-modal';
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
running: 'badge-green',
|
running: 'badge-green',
|
||||||
@@ -19,6 +20,7 @@ const statusColors: Record<string, string> = {
|
|||||||
|
|
||||||
export default function AppsPage() {
|
export default function AppsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const { data: apps = [], isLoading } = useQuery<Application[]>({
|
const { data: apps = [], isLoading } = useQuery<Application[]>({
|
||||||
queryKey: ['applications'],
|
queryKey: ['applications'],
|
||||||
@@ -122,7 +124,10 @@ export default function AppsPage() {
|
|||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<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"
|
className="text-xs px-3 py-1.5 rounded-lg text-red-600 hover:bg-red-50 transition-colors"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
@@ -138,3 +138,19 @@
|
|||||||
.animate-slide-up {
|
.animate-slide-up {
|
||||||
animation: slideUp 0.4s ease-out;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { AlertTriangle, Trash2, Info, X } from 'lucide-react';
|
||||||
|
|
||||||
|
/* ─── Types ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
type ModalVariant = 'danger' | 'warning' | 'info';
|
||||||
|
|
||||||
|
interface ConfirmOptions {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
variant?: ModalVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalContextType {
|
||||||
|
confirm: (options: ConfirmOptions) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Context ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
const ModalContext = createContext<ModalContextType | null>(null);
|
||||||
|
|
||||||
|
export function useConfirm() {
|
||||||
|
const ctx = useContext(ModalContext);
|
||||||
|
if (!ctx) throw new Error('useConfirm must be used within ModalProvider');
|
||||||
|
return ctx.confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Provider ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
export function ModalProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [state, setState] = useState<(ConfirmOptions & { open: boolean }) | null>(null);
|
||||||
|
const resolveRef = useRef<((value: boolean) => void) | null>(null);
|
||||||
|
const cancelBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolveRef.current = resolve;
|
||||||
|
setState({ ...options, open: true });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = useCallback((result: boolean) => {
|
||||||
|
setState(null);
|
||||||
|
resolveRef.current?.(result);
|
||||||
|
resolveRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close on Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state?.open) return;
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') handleClose(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handler);
|
||||||
|
return () => document.removeEventListener('keydown', handler);
|
||||||
|
}, [state?.open, handleClose]);
|
||||||
|
|
||||||
|
// Auto-focus cancel button when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (state?.open) {
|
||||||
|
setTimeout(() => cancelBtnRef.current?.focus(), 50);
|
||||||
|
}
|
||||||
|
}, [state?.open]);
|
||||||
|
|
||||||
|
const variant = state?.variant || 'danger';
|
||||||
|
|
||||||
|
const variantStyles = {
|
||||||
|
danger: {
|
||||||
|
icon: <Trash2 className="w-6 h-6 text-red-600" />,
|
||||||
|
iconBg: 'bg-red-100',
|
||||||
|
confirmBtn: 'btn-danger',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
icon: <AlertTriangle className="w-6 h-6 text-amber-600" />,
|
||||||
|
iconBg: 'bg-amber-100',
|
||||||
|
confirmBtn: 'bg-amber-600 text-white hover:bg-amber-700 focus:ring-amber-500',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
icon: <Info className="w-6 h-6 text-primary-600" />,
|
||||||
|
iconBg: 'bg-primary-100',
|
||||||
|
confirmBtn: 'btn-primary',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = variantStyles[variant];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={{ confirm }}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{state?.open && (
|
||||||
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/40 backdrop-blur-sm animate-modal-backdrop"
|
||||||
|
onClick={() => handleClose(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Dialog */}
|
||||||
|
<div
|
||||||
|
className="relative bg-white rounded-2xl shadow-2xl max-w-md w-full p-0 animate-modal-enter"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
>
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleClose(false)}
|
||||||
|
className="absolute top-4 right-4 p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-6 pb-0">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className={`w-12 h-12 rounded-xl ${style.iconBg} flex items-center justify-center mb-4`}>
|
||||||
|
{style.icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 id="modal-title" className="text-lg font-bold text-gray-900 mb-2">
|
||||||
|
{state.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<p className="text-sm text-gray-500 leading-relaxed whitespace-pre-line">
|
||||||
|
{state.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-end gap-3 p-6">
|
||||||
|
<button
|
||||||
|
ref={cancelBtnRef}
|
||||||
|
onClick={() => handleClose(false)}
|
||||||
|
className="btn-secondary"
|
||||||
|
>
|
||||||
|
{state.cancelText || 'Cancel'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleClose(true)}
|
||||||
|
className={`inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl font-semibold text-sm transition-all duration-150 shadow-sm hover:shadow-md active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-offset-2 ${style.confirmBtn}`}
|
||||||
|
>
|
||||||
|
{state.confirmText || 'Confirm'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { ToastContainer } from 'react-toastify';
|
|||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAuthStore } from '@/lib/store';
|
import { useAuthStore } from '@/lib/store';
|
||||||
|
import { ModalProvider } from './confirm-modal';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -28,7 +29,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{children}
|
<ModalProvider>
|
||||||
|
{children}
|
||||||
|
</ModalProvider>
|
||||||
<ToastContainer position="top-right" autoClose={3000} hideProgressBar={false} closeOnClick pauseOnHover theme="light" />
|
<ToastContainer position="top-right" autoClose={3000} hideProgressBar={false} closeOnClick pauseOnHover theme="light" />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user