feat(ux): accessible confirm dialog replaces window.confirm
- new components/ui/confirm.tsx: ConfirmProvider + useConfirm() built on the existing Radix alert-dialog (focus trap, ESC, aria for free) - wired once into the app layout - all 13 native window.confirm() delete/remove guards now use the styled, accessible confirm with a destructive action button
This commit is contained in:
@@ -6,6 +6,7 @@ import { toast } from 'sonner'
|
||||
import { UserCog } from 'lucide-react'
|
||||
import { createAreaLeadRole, assignAreaLead, unassignAreaLead, deleteAreaLeadRole } from '@/lib/actions'
|
||||
import { Card } from './card'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
|
||||
interface AreaLead {
|
||||
@@ -26,6 +27,7 @@ interface Props {
|
||||
|
||||
export function AreaLeadsPanel({ isAdmin, areaLeads: initial, members }: Props) {
|
||||
const router = useRouter()
|
||||
const confirm = useConfirm()
|
||||
const [rows, setRows] = useState<AreaLead[]>(initial)
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
const [d, setD] = useState({ area_code: '', name: '' })
|
||||
@@ -116,7 +118,7 @@ export function AreaLeadsPanel({ isAdmin, areaLeads: initial, members }: Props)
|
||||
</select>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm(`Delete area-lead role "${r.area_name}"?`)) return
|
||||
if (!(await confirm({ title: 'Delete area-lead role', description: `"${r.area_name}" will be permanently removed.`, confirmText: 'Delete', destructive: true }))) return
|
||||
const res = await deleteAreaLeadRole(r.id)
|
||||
if (res.error) { toast.error(res.error); return }
|
||||
setRows(prev => prev.filter(x => x.id !== r.id))
|
||||
|
||||
@@ -5,10 +5,12 @@ import { toast } from 'sonner'
|
||||
import { LayoutGrid } from 'lucide-react'
|
||||
import { createArea, updateArea, deleteArea } from '@/lib/actions'
|
||||
import { Card } from './card'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
import type { Area } from './types'
|
||||
|
||||
export function AreasPanel({ isAdmin, areas: initial }: { isAdmin: boolean; areas: Area[] }) {
|
||||
const confirm = useConfirm()
|
||||
const [areas, setAreas] = useState<Area[]>(initial)
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
const [d, setD] = useState({ code: '', name: '', icon: '' })
|
||||
@@ -57,7 +59,7 @@ export function AreasPanel({ isAdmin, areas: initial }: { isAdmin: boolean; area
|
||||
{isAdmin && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button onClick={async () => { const res = await updateArea({ areaId: a.id, is_archived: !a.is_archived }); if (res.error) { toast.error(res.error); return } setAreas(prev => prev.map(x => x.id === a.id ? { ...x, is_archived: !x.is_archived } : x)) }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-primary hover:text-primary transition">{a.is_archived ? 'Restore' : 'Archive'}</button>
|
||||
<button onClick={async () => { if (!confirm(`Delete area "${a.name}"?`)) return; const res = await deleteArea(a.id); if (res.error) { toast.error(res.error); return } setAreas(prev => prev.filter(x => x.id !== a.id)); toast.success('Deleted') }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
<button onClick={async () => { if (!(await confirm({ title: 'Delete area', description: `"${a.name}" will be permanently removed.`, confirmText: 'Delete', destructive: true }))) return; const res = await deleteArea(a.id); if (res.error) { toast.error(res.error); return } setAreas(prev => prev.filter(x => x.id !== a.id)); toast.success('Deleted') }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toast } from 'sonner'
|
||||
import { Ticket } from 'lucide-react'
|
||||
import { createInvite, updateInvite, deleteInvite } from '@/lib/actions'
|
||||
import { Card } from './card'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
import type { Invite } from './types'
|
||||
|
||||
@@ -14,6 +15,7 @@ function inviteLink(code: string) {
|
||||
}
|
||||
|
||||
export function InvitesPanel({ isAdmin, invites: initial }: { isAdmin: boolean; invites: Invite[] }) {
|
||||
const confirm = useConfirm()
|
||||
const [invites, setInvites] = useState<Invite[]>(initial)
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
const [d, setD] = useState({ code: '', label: '', expires_at: '', max_uses: '' })
|
||||
@@ -75,7 +77,7 @@ export function InvitesPanel({ isAdmin, invites: initial }: { isAdmin: boolean;
|
||||
{isAdmin && (
|
||||
<>
|
||||
<button onClick={async () => { const res = await updateInvite(inv.id, { is_enabled: !inv.is_enabled }); if (res.error) { toast.error(res.error); return } setInvites(prev => prev.map(x => x.id === inv.id ? { ...x, is_enabled: !x.is_enabled } : x)) }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-primary hover:text-primary transition">{inv.is_enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button onClick={async () => { if (!confirm(`Delete invite "${inv.code}"?`)) return; const res = await deleteInvite(inv.id); if (res.error) { toast.error(res.error); return } setInvites(prev => prev.filter(x => x.id !== inv.id)); toast.success('Deleted') }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
<button onClick={async () => { if (!(await confirm({ title: 'Delete invite', description: `"${inv.code}" will be permanently removed.`, confirmText: 'Delete', destructive: true }))) return; const res = await deleteInvite(inv.id); if (res.error) { toast.error(res.error); return } setInvites(prev => prev.filter(x => x.id !== inv.id)); toast.success('Deleted') }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,12 @@ import { toast } from 'sonner'
|
||||
import { ListChecks } from 'lucide-react'
|
||||
import { createOnboardingStep, updateOnboardingStep, deleteOnboardingStep } from '@/lib/actions'
|
||||
import { Card } from './card'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
import type { Step } from './types'
|
||||
|
||||
export function OnboardingPanel({ isAdmin, steps: initial }: { isAdmin: boolean; steps: Step[] }) {
|
||||
const confirm = useConfirm()
|
||||
const [steps, setSteps] = useState<Step[]>(initial)
|
||||
|
||||
return (
|
||||
@@ -65,7 +67,7 @@ export function OnboardingPanel({ isAdmin, steps: initial }: { isAdmin: boolean;
|
||||
<span className="font-mono text-[10px] text-muted-foreground uppercase tracking-widest">Required</span>
|
||||
</label>
|
||||
{!s.is_system && (
|
||||
<button onClick={async () => { if (!confirm(`Delete step "${s.title}"?`)) return; const res = await deleteOnboardingStep(s.id); if (res.error) { toast.error(res.error); return } setSteps(prev => prev.filter(x => x.id !== s.id)); toast.success('Deleted') }} className="ml-auto font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
<button onClick={async () => { if (!(await confirm({ title: 'Delete step', description: `"${s.title}" will be permanently removed.`, confirmText: 'Delete', destructive: true }))) return; const res = await deleteOnboardingStep(s.id); if (res.error) { toast.error(res.error); return } setSteps(prev => prev.filter(x => x.id !== s.id)); toast.success('Deleted') }} className="ml-auto font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,11 +7,13 @@ import { upsertRoleLabel, createCustomRole, updateCustomRole, deleteCustomRole }
|
||||
import { BUILTIN_ROLES, DEFAULT_ROLE_LABELS } from '@/lib/role-labels'
|
||||
import { Tags } from 'lucide-react'
|
||||
import { Card } from './card'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
import type { RoleLabelRow, CustomRole } from './types'
|
||||
|
||||
export function RolesPanel({ isAdmin, roleLabels, customRoles: initial }: { isAdmin: boolean; roleLabels: RoleLabelRow[]; customRoles: CustomRole[] }) {
|
||||
const router = useRouter()
|
||||
const confirm = useConfirm()
|
||||
const [customRoles, setCustomRoles] = useState<CustomRole[]>(initial)
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
const [draft, setDraft] = useState({ slug: '', name: '', color: '#d4ff00', description: '' })
|
||||
@@ -142,7 +144,7 @@ export function RolesPanel({ isAdmin, roleLabels, customRoles: initial }: { isAd
|
||||
/>
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={async () => { if (!confirm(`Delete role "${r.name}"?`)) return; const res = await deleteCustomRole(r.id); if (res.error) { toast.error(res.error); return } setCustomRoles(prev => prev.filter(x => x.id !== r.id)); toast.success('Deleted') }}
|
||||
onClick={async () => { if (!(await confirm({ title: 'Delete role', description: `"${r.name}" will be permanently removed.`, confirmText: 'Delete', destructive: true }))) return; const res = await deleteCustomRole(r.id); if (res.error) { toast.error(res.error); return } setCustomRoles(prev => prev.filter(x => x.id !== r.id)); toast.success('Deleted') }}
|
||||
className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition"
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -5,10 +5,12 @@ import { toast } from 'sonner'
|
||||
import { CreditCard } from 'lucide-react'
|
||||
import { createTier, updateTier, deleteTier } from '@/lib/actions'
|
||||
import { Card } from './card'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||
import type { Tier } from './types'
|
||||
|
||||
export function TiersPanel({ isAdmin, tiers: initial }: { isAdmin: boolean; tiers: Tier[] }) {
|
||||
const confirm = useConfirm()
|
||||
const [tiers, setTiers] = useState<Tier[]>(initial)
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
const [d, setD] = useState({ slug: '', name: '', dollars: '0', cadence: 'monthly' as Tier['billing_cadence'], description: '' })
|
||||
@@ -75,7 +77,7 @@ export function TiersPanel({ isAdmin, tiers: initial }: { isAdmin: boolean; tier
|
||||
{t.is_system ? (
|
||||
<button onClick={async () => { const res = await updateTier(t.id, { is_archived: !t.is_archived }); if (res.error) { toast.error(res.error); return } setTiers(prev => prev.map(x => x.id === t.id ? { ...x, is_archived: !x.is_archived } : x)) }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-primary hover:text-primary transition">{t.is_archived ? 'Restore' : 'Archive'}</button>
|
||||
) : (
|
||||
<button onClick={async () => { if (!confirm(`Delete tier "${t.name}"?`)) return; const res = await deleteTier(t.id); if (res.error) { toast.error(res.error); return } setTiers(prev => prev.filter(x => x.id !== t.id)); toast.success('Tier deleted') }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
<button onClick={async () => { if (!(await confirm({ title: 'Delete tier', description: `"${t.name}" will be permanently removed.`, confirmText: 'Delete', destructive: true }))) return; const res = await deleteTier(t.id); if (res.error) { toast.error(res.error); return } setTiers(prev => prev.filter(x => x.id !== t.id)); toast.success('Tier deleted') }} className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-red-500 hover:text-red-500 transition">Delete</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user