feat(web): show Re-pair/Delete on accounts list cards

Surface state-aware quick actions directly on each card so the user
doesn't have to drill into the detail page just to delete or re-pair an
account. Re-pair shows when status != connected; Delete (with
destructive confirm dialog) is always available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 00:35:01 +08:00
parent 4d2531689b
commit 2ef64c9192

View File

@ -1,16 +1,25 @@
import Link from "next/link"; import Link from "next/link";
import { PlusIcon, SmartphoneIcon, CalendarIcon } from "lucide-react"; import { PlusIcon, SmartphoneIcon, CalendarIcon, PowerIcon, Trash2Icon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription,
} from "@/components/ui/card"; } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { AccountStatusBadge } from "@/components/account-status-badge"; import { AccountStatusBadge } from "@/components/account-status-badge";
import { getSeededOperator } from "@/lib/operator"; import { getSeededOperator } from "@/lib/operator";
import { listAccounts } from "@/lib/queries"; import { listAccounts } from "@/lib/queries";
import { pairAccountAction, deleteAccountAction } from "@/actions/accounts";
export default async function AccountsPage() { export default async function AccountsPage() {
const op = await getSeededOperator(); const op = await getSeededOperator();
@ -25,7 +34,7 @@ export default async function AccountsPage() {
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Link href={"/accounts/new" as any}> <Link href={"/accounts/new" as any}>
<PlusIcon /> <PlusIcon />
Pair New Account Add Account
</Link> </Link>
</Button> </Button>
</div> </div>
@ -34,27 +43,29 @@ export default async function AccountsPage() {
{accounts.length > 0 ? ( {accounts.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{accounts.map((account) => ( {accounts.map((account) => (
<Link <Card
key={account.id} key={account.id}
// eslint-disable-next-line @typescript-eslint/no-explicit-any className="h-full transition-shadow hover:shadow-md hover:ring-foreground/20"
href={`/accounts/${account.id}` as any}
className="block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
> >
<Card className="h-full transition-shadow hover:shadow-md hover:ring-foreground/20">
<CardHeader> <CardHeader>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<CardTitle className="text-base leading-snug">{account.label}</CardTitle> <CardTitle className="text-base leading-snug">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Link href={`/accounts/${account.id}` as any} className="hover:underline">
{account.label}
</Link>
</CardTitle>
<AccountStatusBadge status={account.status} /> <AccountStatusBadge status={account.status} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-3">
{account.phoneNumber ? ( {account.phoneNumber ? (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground"> <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<SmartphoneIcon className="size-3.5 shrink-0" /> <SmartphoneIcon className="size-3.5 shrink-0" />
<span>{account.phoneNumber}</span> <span>{account.phoneNumber}</span>
</div> </div>
) : ( ) : (
<p className="text-sm text-muted-foreground/60 italic">No phone number</p> <p className="text-sm text-muted-foreground/60 italic">Not paired yet</p>
)} )}
{account.lastConnectedAt ? ( {account.lastConnectedAt ? (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground"> <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
@ -70,9 +81,54 @@ export default async function AccountsPage() {
</span> </span>
</div> </div>
) : null} ) : null}
{/* Quick actions */}
<div className="flex items-center gap-2 pt-1">
{account.status !== "connected" && (
<form action={pairAccountAction}>
<input type="hidden" name="accountId" value={account.id} />
<Button type="submit" size="sm" variant="default">
<PowerIcon />
{account.status === "unpaired" ? "Pair" : "Re-pair"}
</Button>
</form>
)}
<Button asChild size="sm" variant="outline">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Link href={`/accounts/${account.id}` as any}>Open</Link>
</Button>
<Dialog>
<DialogTrigger asChild>
<Button
size="sm"
variant="ghost"
className="text-destructive hover:bg-destructive/10 hover:text-destructive ml-auto"
>
<Trash2Icon />
Delete
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete this account?</DialogTitle>
<DialogDescription>
<strong>{account.label}</strong> and all its reminders, groups,
and history will be permanently removed. 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>
</div>
</CardContent> </CardContent>
</Card> </Card>
</Link>
))} ))}
</div> </div>
) : ( ) : (
@ -89,7 +145,7 @@ export default async function AccountsPage() {
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Link href={"/accounts/new" as any}> <Link href={"/accounts/new" as any}>
<PlusIcon /> <PlusIcon />
Pair New Account Add Account
</Link> </Link>
</Button> </Button>
</CardContent> </CardContent>