👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

deck deletion!

+210 -1
+28 -1
src/components/deck/DeckActionsMenu.tsx
··· 1 1 import { useQueryClient } from "@tanstack/react-query"; 2 - import { MoreVertical } from "lucide-react"; 2 + import { MoreVertical, Trash2 } from "lucide-react"; 3 3 import { useEffect, useRef, useState } from "react"; 4 4 import { toast } from "sonner"; 5 + import { DeleteDeckDialog } from "@/components/deck/DeleteDeckDialog"; 6 + import type { Rkey } from "@/lib/atproto-client"; 5 7 import { getCardDataProvider } from "@/lib/card-data-provider"; 6 8 import { prefetchCards } from "@/lib/card-prefetch"; 9 + import { useDeleteDeckMutation } from "@/lib/deck-queries"; 7 10 import type { Deck } from "@/lib/deck-types"; 8 11 import { 9 12 findAllCanonicalPrintings, ··· 14 17 15 18 interface DeckActionsMenuProps { 16 19 deck: Deck; 20 + rkey: Rkey; 17 21 onUpdateDeck: (updater: (prev: Deck) => Deck) => Promise<void>; 18 22 onCardsChanged?: (changedIds: Set<ScryfallId>) => void; 19 23 } 20 24 21 25 export function DeckActionsMenu({ 22 26 deck, 27 + rkey, 23 28 onUpdateDeck, 24 29 onCardsChanged, 25 30 }: DeckActionsMenuProps) { 26 31 const queryClient = useQueryClient(); 27 32 const [isOpen, setIsOpen] = useState(false); 33 + const [showDeleteDialog, setShowDeleteDialog] = useState(false); 28 34 const menuRef = useRef<HTMLDivElement>(null); 35 + const deleteMutation = useDeleteDeckMutation(rkey); 29 36 30 37 useEffect(() => { 31 38 const handleClickOutside = (event: MouseEvent) => { ··· 113 120 > 114 121 Set all to best 115 122 </button> 123 + <div className="border-t border-gray-200 dark:border-gray-700" /> 124 + <button 125 + type="button" 126 + onClick={() => { 127 + setIsOpen(false); 128 + setShowDeleteDialog(true); 129 + }} 130 + className="w-full text-left px-4 py-3 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-red-600 dark:text-red-400 text-sm flex items-center gap-2" 131 + > 132 + <Trash2 size={14} /> 133 + Delete deck 134 + </button> 116 135 </div> 117 136 )} 137 + 138 + <DeleteDeckDialog 139 + deckName={deck.name} 140 + isOpen={showDeleteDialog} 141 + onClose={() => setShowDeleteDialog(false)} 142 + onConfirm={() => deleteMutation.mutate()} 143 + isDeleting={deleteMutation.isPending} 144 + /> 118 145 </div> 119 146 ); 120 147 }
+136
src/components/deck/DeleteDeckDialog.tsx
··· 1 + import { AlertTriangle } from "lucide-react"; 2 + import { useEffect, useId, useState } from "react"; 3 + 4 + interface DeleteDeckDialogProps { 5 + deckName: string; 6 + isOpen: boolean; 7 + onClose: () => void; 8 + onConfirm: () => void; 9 + isDeleting?: boolean; 10 + } 11 + 12 + export function DeleteDeckDialog({ 13 + deckName, 14 + isOpen, 15 + onClose, 16 + onConfirm, 17 + isDeleting = false, 18 + }: DeleteDeckDialogProps) { 19 + const [confirmText, setConfirmText] = useState(""); 20 + const titleId = useId(); 21 + const inputId = useId(); 22 + 23 + const isMatch = confirmText === deckName; 24 + 25 + useEffect(() => { 26 + if (!isOpen) { 27 + setConfirmText(""); 28 + } 29 + }, [isOpen]); 30 + 31 + useEffect(() => { 32 + const handleKeyDown = (e: KeyboardEvent) => { 33 + if (e.key === "Escape" && !isDeleting) { 34 + onClose(); 35 + } 36 + }; 37 + 38 + if (isOpen) { 39 + document.addEventListener("keydown", handleKeyDown); 40 + return () => document.removeEventListener("keydown", handleKeyDown); 41 + } 42 + }, [isOpen, isDeleting, onClose]); 43 + 44 + if (!isOpen) return null; 45 + 46 + const handleSubmit = (e: React.FormEvent) => { 47 + e.preventDefault(); 48 + if (isMatch && !isDeleting) { 49 + onConfirm(); 50 + } 51 + }; 52 + 53 + return ( 54 + <> 55 + {/* Backdrop */} 56 + <div 57 + className="fixed inset-0 bg-black/50 z-40" 58 + onClick={isDeleting ? undefined : onClose} 59 + aria-hidden="true" 60 + /> 61 + 62 + {/* Dialog */} 63 + <div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none"> 64 + <div 65 + role="alertdialog" 66 + aria-modal="true" 67 + aria-labelledby={titleId} 68 + className="bg-white dark:bg-slate-900 rounded-lg shadow-2xl max-w-md w-full pointer-events-auto border border-gray-300 dark:border-slate-700" 69 + > 70 + {/* Header */} 71 + <div className="flex items-center gap-3 p-6 border-b border-gray-200 dark:border-slate-800"> 72 + <div className="p-2 bg-red-100 dark:bg-red-900/30 rounded-full"> 73 + <AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400" /> 74 + </div> 75 + <h2 76 + id={titleId} 77 + className="text-xl font-bold text-gray-900 dark:text-white" 78 + > 79 + Delete deck 80 + </h2> 81 + </div> 82 + 83 + {/* Body */} 84 + <form onSubmit={handleSubmit} className="p-6 space-y-4"> 85 + <p className="text-gray-600 dark:text-gray-400"> 86 + This action <strong>cannot</strong> be undone. This will 87 + permanently delete the deck and all its cards. 88 + </p> 89 + 90 + <div> 91 + <label 92 + htmlFor={inputId} 93 + className="block text-sm text-gray-700 dark:text-gray-300 mb-2" 94 + > 95 + Please type{" "} 96 + <span className="font-mono font-semibold text-gray-900 dark:text-white bg-gray-100 dark:bg-slate-800 px-1.5 py-0.5 rounded"> 97 + {deckName} 98 + </span>{" "} 99 + to confirm. 100 + </label> 101 + <input 102 + id={inputId} 103 + type="text" 104 + value={confirmText} 105 + onChange={(e) => setConfirmText(e.target.value)} 106 + disabled={isDeleting} 107 + autoComplete="off" 108 + className="w-full px-4 py-2 bg-white dark:bg-slate-800 border border-gray-300 dark:border-slate-700 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed" 109 + placeholder="Deck name" 110 + /> 111 + </div> 112 + 113 + {/* Footer */} 114 + <div className="flex items-center justify-end gap-3 pt-2"> 115 + <button 116 + type="button" 117 + onClick={onClose} 118 + disabled={isDeleting} 119 + className="px-4 py-2 bg-gray-200 dark:bg-slate-800 hover:bg-gray-300 dark:hover:bg-slate-700 text-gray-900 dark:text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 120 + > 121 + Cancel 122 + </button> 123 + <button 124 + type="submit" 125 + disabled={!isMatch || isDeleting} 126 + className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-600/50 disabled:cursor-not-allowed text-white rounded-lg transition-colors" 127 + > 128 + {isDeleting ? "Deleting..." : "Delete this deck"} 129 + </button> 130 + </div> 131 + </form> 132 + </div> 133 + </div> 134 + </> 135 + ); 136 + }
+45
src/lib/deck-queries.ts
··· 6 6 import type { Did } from "@atcute/lexicons"; 7 7 import { queryOptions, useQueryClient } from "@tanstack/react-query"; 8 8 import { useNavigate } from "@tanstack/react-router"; 9 + import { toast } from "sonner"; 9 10 import { 10 11 asPdsUrl, 11 12 createDeckRecord, 13 + deleteDeckRecord, 12 14 getDeckRecord, 13 15 type ListRecordsResponse, 14 16 listUserDecks, ··· 174 176 // Slingshot (cache) might be stale anyway 175 177 }); 176 178 } 179 + 180 + /** 181 + * Mutation for deleting a deck 182 + * Invalidates deck list and navigates to profile on success 183 + */ 184 + export function useDeleteDeckMutation(rkey: Rkey) { 185 + const { agent, session } = useAuth(); 186 + const queryClient = useQueryClient(); 187 + const navigate = useNavigate(); 188 + 189 + return useMutationWithToast({ 190 + mutationFn: async () => { 191 + if (!agent || !session) { 192 + throw new Error("Must be authenticated to delete a deck"); 193 + } 194 + 195 + const result = await deleteDeckRecord(agent, rkey); 196 + 197 + if (!result.success) { 198 + throw result.error; 199 + } 200 + 201 + return result.data; 202 + }, 203 + onSuccess: () => { 204 + toast.success("Deck deleted"); 205 + 206 + if (!session) return; 207 + 208 + // Invalidate deck list for current user 209 + queryClient.invalidateQueries({ 210 + queryKey: ["decks", session.info.sub], 211 + }); 212 + 213 + // Navigate back to profile 214 + navigate({ 215 + to: "/profile/$did", 216 + params: { did: session.info.sub }, 217 + }); 218 + }, 219 + errorMessage: "Failed to delete deck", 220 + }); 221 + }
+1
src/routes/profile/$did/deck/$rkey/index.tsx
··· 532 532 <div className="flex items-center gap-2"> 533 533 <DeckActionsMenu 534 534 deck={deck} 535 + rkey={asRkey(rkey)} 535 536 onUpdateDeck={updateDeck} 536 537 onCardsChanged={handleCardsChanged} 537 538 />