158 lines
5.5 KiB
TypeScript
158 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { Check, Loader2, ShieldBan, X } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog";
|
|
import { AccountAvatar } from "@/components/profile/account-avatar";
|
|
import {
|
|
createAccountLink,
|
|
denyAccountLink,
|
|
denyAndBlockAccountLink,
|
|
} from "@/lib/actions/account-links";
|
|
import type { PendingLinkRequestDTO } from "@/types/profile";
|
|
|
|
type DenyIntent = { request: PendingLinkRequestDTO; block: boolean };
|
|
|
|
/**
|
|
* "Requested Account Link" section: link requests this account received.
|
|
* Only the recipient acts on these -- the requester just sees
|
|
* "awaiting confirmation" on their own Profile page.
|
|
*/
|
|
export function PendingLinkRequests({ requests }: { requests: PendingLinkRequestDTO[] }) {
|
|
// Which request row (if any) is currently processing a button press.
|
|
const [busyRequestId, setBusyRequestId] = useState<string | null>(null);
|
|
const [denyIntent, setDenyIntent] = useState<DenyIntent | null>(null);
|
|
|
|
/** Runs a request action, toasting its error (if any) and reporting
|
|
* success so the caller can toast a specific confirmation. */
|
|
async function run(requestId: string, action: () => Promise<{ error?: string } | undefined>) {
|
|
setBusyRequestId(requestId);
|
|
let failed = false;
|
|
try {
|
|
const result = await action();
|
|
if (result?.error) {
|
|
toast.error(result.error);
|
|
failed = true;
|
|
}
|
|
} catch {
|
|
toast.error("That didn't work. Try again.");
|
|
failed = true;
|
|
} finally {
|
|
setBusyRequestId(null);
|
|
}
|
|
return !failed;
|
|
}
|
|
|
|
async function handleConfirmLink(request: PendingLinkRequestDTO) {
|
|
const ok = await run(request.requestId, () => createAccountLink(request.requestId));
|
|
if (ok) toast.success("Account link created.");
|
|
}
|
|
|
|
async function handleDeny() {
|
|
if (!denyIntent) return;
|
|
const { request, block } = denyIntent;
|
|
setDenyIntent(null);
|
|
const ok = await run(
|
|
request.requestId,
|
|
block
|
|
? () => denyAndBlockAccountLink(request.requestId)
|
|
: () => denyAccountLink(request.requestId)
|
|
);
|
|
if (!ok) return;
|
|
toast.success(
|
|
block
|
|
? "Request denied — that account can't send link requests to you anymore."
|
|
: "Request denied."
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Requested Account Link</CardTitle>
|
|
<CardDescription>
|
|
These accounts asked to link with this one. Linking lets each
|
|
account toggle the other in.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-3">
|
|
{requests.map((request) => {
|
|
const busy = busyRequestId === request.requestId;
|
|
return (
|
|
<div
|
|
key={request.requestId}
|
|
className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center"
|
|
>
|
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
<AccountAvatar account={request} sizeClass="size-9" />
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-medium">{request.email}</p>
|
|
<p className="truncate text-xs text-muted-foreground">
|
|
{request.name ? `${request.name} · ` : ""}
|
|
requested {request.requestedAtLabel}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleConfirmLink(request)}
|
|
disabled={busyRequestId !== null}
|
|
>
|
|
{busy ? (
|
|
<Loader2 className="size-3.5 animate-spin" />
|
|
) : (
|
|
<Check className="size-3.5" />
|
|
)}
|
|
Create Account Link
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={busyRequestId !== null}
|
|
onClick={() => setDenyIntent({ request, block: false })}
|
|
>
|
|
<X className="size-3.5" />
|
|
Deny Account Link
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
disabled={busyRequestId !== null}
|
|
onClick={() => setDenyIntent({ request, block: true })}
|
|
>
|
|
<ShieldBan className="size-3.5" />
|
|
Deny and Block Account Link
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
|
|
<ConfirmDeleteDialog
|
|
open={denyIntent !== null}
|
|
onOpenChange={(open) => !open && setDenyIntent(null)}
|
|
title={denyIntent?.block ? "Deny and block this account?" : "Deny this request?"}
|
|
description={
|
|
denyIntent?.block
|
|
? `Deny the link request from ${denyIntent?.request?.email} and block that account from sending any more link requests to this one. You can lift the block later from the Blocked Account Link Requests section.`
|
|
: `Deny the link request from ${denyIntent?.request?.email}. They can request again later unless you block them.`
|
|
}
|
|
confirmLabel={denyIntent?.block ? "Deny and Block" : "Deny"}
|
|
onConfirm={handleDeny}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|