fix(web): client-component delete/unpair cards on accounts/[id]
The DialogTrigger asChild + transparent button overlay pattern wasn't
emitting a clickable button in the rendered DOM under radix-ui 1.4 +
Next 16 (server component context), so Delete and Unpair both became
no-ops. Replace each with a small client component that:
- holds open-state for the confirm Dialog
- drives the Card itself as the click target via role='button',
tabIndex, onClick, and Enter/Space keydown handlers
- calls the server action through useTransition
The Card stays a div (no <button> wrapping a Card → satisfies the
existing static-guard test). Removed the unused inline Dialog imports
and unpair/delete icons from the page.
Also trim the forgot-password dialog body to one sentence per request
('don't write too detail').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d583d9194
commit
6759ca8131
104
apps/web/src/app/accounts/[id]/delete-account-card.tsx
Normal file
104
apps/web/src/app/accounts/[id]/delete-account-card.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { ChevronRightIcon, Loader2Icon, Trash2Icon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { deleteAccountAction } from "@/actions/accounts";
|
||||
|
||||
interface DeleteAccountCardProps {
|
||||
accountId: string;
|
||||
accountLabel: string;
|
||||
}
|
||||
|
||||
export function DeleteAccountCard({
|
||||
accountId,
|
||||
accountLabel,
|
||||
}: DeleteAccountCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, start] = useTransition();
|
||||
|
||||
function confirm() {
|
||||
start(async () => {
|
||||
const fd = new FormData();
|
||||
fd.append("accountId", accountId);
|
||||
await deleteAccountAction(fd);
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Card
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Delete account"
|
||||
onClick={() => setOpen(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
className="transition-all hover:shadow-md hover:ring-destructive/30 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive focus-visible:ring-offset-2"
|
||||
>
|
||||
<CardContent className="flex items-center justify-between gap-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-destructive/10">
|
||||
<Trash2Icon className="size-4 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
Delete Account
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Remove the account and all its reminders, groups, and history
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete this account permanently?</DialogTitle>
|
||||
<DialogDescription>
|
||||
<strong>{accountLabel}</strong> will be removed along with its
|
||||
synced groups, scheduled reminders, and all run history. This
|
||||
cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={pending}
|
||||
onClick={confirm}
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2Icon className="size-4" />
|
||||
)}
|
||||
Yes, delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,6 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
UsersIcon,
|
||||
Trash2Icon,
|
||||
ArrowLeftIcon,
|
||||
SmartphoneIcon,
|
||||
CalendarIcon,
|
||||
@ -10,7 +9,6 @@ import {
|
||||
DatabaseIcon,
|
||||
PencilIcon,
|
||||
PowerIcon,
|
||||
PowerOffIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -20,23 +18,12 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { AccountStatusBadge } from "@/components/account-status-badge";
|
||||
import { getSeededOperator } from "@/lib/operator";
|
||||
import { getAccount } from "@/lib/queries";
|
||||
import {
|
||||
unpairAccountAction,
|
||||
pairAccountAction,
|
||||
deleteAccountAction,
|
||||
} from "@/actions/accounts";
|
||||
import { pairAccountAction } from "@/actions/accounts";
|
||||
import { DeleteAccountCard } from "./delete-account-card";
|
||||
import { UnpairAccountCard } from "./unpair-account-card";
|
||||
|
||||
interface AccountDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@ -156,110 +143,11 @@ export default async function AccountDetailPage({ params }: AccountDetailPagePro
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* Unpair — transparent <button> overlay (sibling of Card,
|
||||
inside a relative wrapper). Same pattern as Delete below. */}
|
||||
<Dialog>
|
||||
<div className="relative">
|
||||
<Card className="transition-all hover:shadow-md hover:ring-amber-500/30 cursor-pointer">
|
||||
<CardContent className="flex items-center justify-between gap-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-amber-500/10">
|
||||
<PowerOffIcon className="size-4 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Unpair</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Disconnect from WhatsApp; keep the account so you can re-pair later
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Unpair WhatsApp"
|
||||
className="absolute inset-0 w-full rounded-xl bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Unpair this account?</DialogTitle>
|
||||
<DialogDescription>
|
||||
<strong>{account.label}</strong> will disconnect from WhatsApp and
|
||||
scheduled reminders using it will stop firing until you re-pair.
|
||||
The account itself is kept; reminders and other data are not deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
<form action={unpairAccountAction}>
|
||||
<input type="hidden" name="accountId" value={account.id} />
|
||||
<Button type="submit" variant="default" size="sm">
|
||||
<PowerOffIcon />
|
||||
Yes, unpair
|
||||
</Button>
|
||||
</form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<UnpairAccountCard accountId={account.id} accountLabel={account.label} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Delete — transparent <button> overlay opens the dialog.
|
||||
The button lives as a sibling of <Card> (inside a relative
|
||||
wrapper) instead of inside the Card. Radix's asChild-driven
|
||||
DialogTrigger stops emitting the underlying button when the
|
||||
wrapper Card adds an `absolute inset-0` sibling on the same
|
||||
stacking context, so we mirror the working pattern from the
|
||||
Pair/Re-pair card above. */}
|
||||
<Dialog>
|
||||
<div className="relative">
|
||||
<Card className="transition-all hover:shadow-md hover:ring-destructive/30 cursor-pointer">
|
||||
<CardContent className="flex items-center justify-between gap-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-destructive/10">
|
||||
<Trash2Icon className="size-4 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-destructive">Delete Account</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Remove the account and all its reminders, groups, and history
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete account"
|
||||
className="absolute inset-0 w-full rounded-xl bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive focus-visible:ring-offset-2"
|
||||
/>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete this account permanently?</DialogTitle>
|
||||
<DialogDescription>
|
||||
<strong>{account.label}</strong> will be removed along with its
|
||||
synced groups, scheduled reminders, and all run history. This cannot be
|
||||
undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
<form action={deleteAccountAction}>
|
||||
<input type="hidden" name="accountId" value={account.id} />
|
||||
<Button type="submit" variant="destructive" size="sm">
|
||||
<Trash2Icon />
|
||||
Yes, delete
|
||||
</Button>
|
||||
</form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DeleteAccountCard accountId={account.id} accountLabel={account.label} />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
||||
102
apps/web/src/app/accounts/[id]/unpair-account-card.tsx
Normal file
102
apps/web/src/app/accounts/[id]/unpair-account-card.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { ChevronRightIcon, Loader2Icon, PowerOffIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { unpairAccountAction } from "@/actions/accounts";
|
||||
|
||||
interface UnpairAccountCardProps {
|
||||
accountId: string;
|
||||
accountLabel: string;
|
||||
}
|
||||
|
||||
export function UnpairAccountCard({
|
||||
accountId,
|
||||
accountLabel,
|
||||
}: UnpairAccountCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, start] = useTransition();
|
||||
|
||||
function confirm() {
|
||||
start(async () => {
|
||||
const fd = new FormData();
|
||||
fd.append("accountId", accountId);
|
||||
await unpairAccountAction(fd);
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Card
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Unpair WhatsApp"
|
||||
onClick={() => setOpen(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
className="transition-all hover:shadow-md hover:ring-amber-500/30 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<CardContent className="flex items-center justify-between gap-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-9 items-center justify-center rounded-lg bg-amber-500/10">
|
||||
<PowerOffIcon className="size-4 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Unpair</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Disconnect from WhatsApp; keep the account so you can re-pair later
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Unpair this account?</DialogTitle>
|
||||
<DialogDescription>
|
||||
<strong>{accountLabel}</strong> will disconnect from WhatsApp and
|
||||
scheduled reminders using it will stop firing until you re-pair.
|
||||
The account itself is kept; reminders and other data are not
|
||||
deleted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={pending}
|
||||
onClick={confirm}
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<PowerOffIcon className="size-4" />
|
||||
)}
|
||||
Yes, unpair
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -84,14 +84,7 @@ export function LoginFormClient({ next }: { next: string }) {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Forgot your password?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Self-service password reset is intentionally disabled on
|
||||
this deployment. To recover access, contact an
|
||||
administrator. They can reset your password from
|
||||
Settings → Users, or run{" "}
|
||||
<code className="font-mono text-[0.75rem]">
|
||||
./scripts/set-password.sh <username>
|
||||
</code>{" "}
|
||||
from the tools container.
|
||||
Contact your administrator to reset it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user