eny.space Landingpage
1
fork

Configure Feed

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

feat(account-deletion): add another confirmation step with user input as the final answer

+67 -57
+67 -57
app/dashboard/user-dashboard-client.tsx
··· 196 196 handle: string; 197 197 email?: string; 198 198 indexedAt?: string; 199 - deactivatedAt?: string; 200 199 }; 201 200 201 + function DeleteDialog({ 202 + handle, 203 + onConfirm, 204 + onCancel, 205 + busy, 206 + }: { 207 + handle: string; 208 + onConfirm: () => void; 209 + onCancel: () => void; 210 + busy: boolean; 211 + }) { 212 + const [input, setInput] = useState(""); 213 + const matches = input === handle; 214 + 215 + return ( 216 + <div className="mt-2 space-y-3 rounded-md border border-rose-500/30 bg-rose-950/20 p-3 text-sm"> 217 + <div className="space-y-1"> 218 + <Paragraph className="font-medium text-rose-300">Delete {handle}?</Paragraph> 219 + <Paragraph className="text-xs text-white/50"> 220 + All data is permanently and immediately deleted. This cannot be undone. 221 + </Paragraph> 222 + </div> 223 + <div className="space-y-1"> 224 + <Paragraph className="text-xs text-white/50">Type the handle to confirm:</Paragraph> 225 + <Input 226 + value={input} 227 + onChange={(e) => setInput(e.target.value)} 228 + placeholder={handle} 229 + className="h-7 text-xs" 230 + autoFocus 231 + /> 232 + </div> 233 + <div className="flex gap-2"> 234 + <Button 235 + onClick={onConfirm} 236 + disabled={!matches || busy} 237 + className="h-7 rounded-full px-3 text-xs bg-rose-600 hover:bg-rose-500 border-0 flex-1" 238 + > 239 + {busy ? "Deleting…" : "Delete permanently"} 240 + </Button> 241 + <Button 242 + onClick={onCancel} 243 + disabled={busy} 244 + className="h-7 rounded-full px-3 text-xs bg-transparent border border-white/20 hover:bg-white/5" 245 + > 246 + Cancel 247 + </Button> 248 + </div> 249 + </div> 250 + ); 251 + } 252 + 202 253 function AccountRow({ account, onRefresh }: { account: PdsAccount; onRefresh: () => void }) { 203 - const [confirmDelete, setConfirmDelete] = useState(false); 254 + const [showDeleteDialog, setShowDeleteDialog] = useState(false); 204 255 const [busy, setBusy] = useState(false); 205 256 const [rowError, setRowError] = useState<string | null>(null); 206 - const isDeactivated = Boolean(account.deactivatedAt); 207 - 208 - const deactivate = async () => { 209 - setBusy(true); 210 - setRowError(null); 211 - try { 212 - const res = await fetch("/api/pds/atproto/accounts", { 213 - method: "PATCH", 214 - headers: { "Content-Type": "application/json" }, 215 - body: JSON.stringify({ did: account.did, active: isDeactivated }), 216 - }); 217 - const payload = await res.json().catch(() => ({})); 218 - if (!res.ok) throw new Error(payload?.message || "Failed to update account"); 219 - onRefresh(); 220 - } catch (e) { 221 - setRowError(e instanceof Error ? e.message : String(e)); 222 - } finally { 223 - setBusy(false); 224 - } 225 - }; 226 257 227 258 const deleteAccount = async () => { 228 259 setBusy(true); ··· 238 269 onRefresh(); 239 270 } catch (e) { 240 271 setRowError(e instanceof Error ? e.message : String(e)); 241 - setConfirmDelete(false); 272 + setShowDeleteDialog(false); 242 273 } finally { 243 274 setBusy(false); 244 275 } ··· 248 279 <div className="py-3 text-sm space-y-1"> 249 280 <div className="flex items-start justify-between gap-4"> 250 281 <div className="space-y-0.5 min-w-0"> 251 - <div className="flex items-center gap-2"> 252 - <Paragraph className="font-medium text-white truncate">{account.handle}</Paragraph> 253 - {isDeactivated && ( 254 - <span className="text-xs text-amber-400/80 border border-amber-400/30 rounded px-1">deactivated</span> 255 - )} 256 - </div> 282 + <Paragraph className="font-medium text-white truncate">{account.handle}</Paragraph> 257 283 {account.email && ( 258 284 <Paragraph className="text-xs text-white/50 truncate">{account.email}</Paragraph> 259 285 )} ··· 266 292 </Paragraph> 267 293 )} 268 294 <button 269 - onClick={deactivate} 270 - disabled={busy} 271 - className="text-xs text-white/40 hover:text-amber-300 transition-colors disabled:opacity-40" 295 + onClick={() => setShowDeleteDialog(true)} 296 + disabled={busy || showDeleteDialog} 297 + className="text-xs text-white/40 hover:text-rose-400 transition-colors disabled:opacity-40" 272 298 > 273 - {busy ? "…" : isDeactivated ? "Reactivate" : "Deactivate"} 299 + Delete 274 300 </button> 275 - {confirmDelete ? ( 276 - <div className="flex items-center gap-1"> 277 - <button 278 - onClick={deleteAccount} 279 - disabled={busy} 280 - className="text-xs text-rose-400 hover:text-rose-300 transition-colors disabled:opacity-40" 281 - > 282 - Confirm 283 - </button> 284 - <button 285 - onClick={() => setConfirmDelete(false)} 286 - className="text-xs text-white/30 hover:text-white/60 transition-colors" 287 - > 288 - Cancel 289 - </button> 290 - </div> 291 - ) : ( 292 - <button 293 - onClick={() => setConfirmDelete(true)} 294 - className="text-xs text-white/40 hover:text-rose-400 transition-colors" 295 - > 296 - Delete 297 - </button> 298 - )} 299 301 </div> 300 302 </div> 303 + {showDeleteDialog && ( 304 + <DeleteDialog 305 + handle={account.handle} 306 + onConfirm={deleteAccount} 307 + onCancel={() => setShowDeleteDialog(false)} 308 + busy={busy} 309 + /> 310 + )} 301 311 {rowError && ( 302 312 <Paragraph className="text-xs text-rose-300 break-all">{rowError}</Paragraph> 303 313 )}