SkateSpots/src/components/DeleteEntityDialog.tsx
shakespeare.diy a6a377f468 Add delete/dissolve for crews and Lab profiles via NIP-09 (v1.0.72)
- New useDeleteEntity hook: publishes a kind:5 deletion request referencing
  the target addressable event by `a` coord, `e` id, and `k` kind. Owner-only.
- New reusable DeleteEntityDialog: owner-gated trigger + confirmation with a
  clear "deletion is best-effort on decentralized relays" warning, redirect,
  and query invalidation so the item drops out of lists.
- Wired "Disband" into CrewDetailPage (founder only, redirects to /crews).
- Wired a "Manage → Delete Profile" card into CoachDetailPage for all Lab
  profile types (coach/filmer/editor/photographer/builder), redirects to /lab.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 20:02:33 +07:00

145 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { Trash2, Loader2, AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useDeleteEntity } from '@/hooks/useDeleteEntity';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
import type { NostrEvent } from '@nostrify/nostrify';
interface DeleteEntityDialogProps {
/** The addressable event to delete (crew, Lab profile, spot, …). */
event: NostrEvent;
/** Human label for the thing being deleted, e.g. "crew" or "coach profile". */
entityLabel: string;
/** The name/title shown in the confirmation copy. */
entityName: string;
/** Where to send the user after a successful delete (e.g. "/crews"). */
redirectTo?: string;
/** Verb shown on the trigger button (default "Delete"). */
triggerText?: string;
/** Optional custom trigger. If omitted a default outline button is rendered. */
trigger?: ReactNode;
/** Extra classes for the default trigger button. */
className?: string;
}
/**
* Reusable "delete / dissolve" control for any addressable event the current
* user owns. Publishes a NIP-09 deletion request (see useDeleteEntity) after an
* explicit confirmation, and is only rendered for the owner.
*/
export function DeleteEntityDialog({
event,
entityLabel,
entityName,
redirectTo,
triggerText = 'Delete',
trigger,
className,
}: DeleteEntityDialogProps) {
const { user } = useCurrentUser();
const { mutateAsync: deleteEntity, isPending } = useDeleteEntity();
const { toast } = useToast();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
// Only the owner can delete their own event.
if (!user || user.pubkey !== event.pubkey) return null;
const handleDelete = async () => {
try {
await deleteEntity({ event, reason: `Deleted ${entityLabel} "${entityName}"` });
toast({
title: 'Deletion requested',
description: `Your ${entityLabel} has been removed. Relays that honor deletions will stop serving it.`,
});
setOpen(false);
if (redirectTo) navigate(redirectTo);
} catch (error) {
toast({
title: 'Delete failed',
description: error instanceof Error ? error.message : 'Something went wrong',
variant: 'destructive',
});
}
};
return (
<>
{trigger ? (
<span onClick={() => setOpen(true)}>{trigger}</span>
) : (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setOpen(true)}
className={cn(
'gap-1.5 border-red-500/40 text-red-400 hover:bg-red-500/10 hover:text-red-300',
className,
)}
>
<Trash2 className="h-3.5 w-3.5" /> {triggerText}
</Button>
)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-red-500" />
Delete "{entityName}"?
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-3">
<p>
This asks relays to remove your {entityLabel}. This can’t be undone from here — you’d
have to create a new one.
</p>
<div className="flex gap-2 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3 text-sm">
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-500" />
<p className="text-yellow-400/90">
Nostr is decentralized, so deletion is best-effort: relays that honor NIP-09 will
drop it, but a relay that doesn’t may keep a copy.
</p>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
Cancel
</Button>
<Button
onClick={handleDelete}
disabled={isPending}
className="bg-red-500 text-white hover:bg-red-600"
>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting…
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
{triggerText}
</>
)}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}