Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions apps/web/src/pages/UserBookDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react'
import { useState, useEffect, useMemo, useRef } from 'react'
import { useParams, Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { useLanguage } from '../context/LanguageContext'
Expand Down Expand Up @@ -27,6 +27,12 @@ export function UserBookDetailPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [deleting, setDeleting] = useState(false)
// Inline two-stage confirm — mirrors VocabularyPage's delete pattern.
// First click flips the icon button into a red "Confirm?" pill; a second
// click within 3 s actually deletes. Avoids the browser-native confirm()
// popup and the prior loud red-circle visual.
const [pendingDelete, setPendingDelete] = useState(false)
const pendingTimeoutRef = useRef<number | null>(null)

// Get saved progress from localStorage
const savedProgress = useMemo((): SavedProgress | null => {
Expand Down Expand Up @@ -93,8 +99,16 @@ export function UserBookDetailPage() {

const handleDelete = async () => {
if (!id || deleting) return
if (!confirm('Are you sure you want to delete this book?')) return

// First click: arm the confirm. Second click within 3 s actually deletes.
if (!pendingDelete) {
setPendingDelete(true)
if (pendingTimeoutRef.current) window.clearTimeout(pendingTimeoutRef.current)
pendingTimeoutRef.current = window.setTimeout(() => setPendingDelete(false), 3000)
return
}

if (pendingTimeoutRef.current) window.clearTimeout(pendingTimeoutRef.current)
setDeleting(true)
try {
await deleteUserBook(id)
Expand All @@ -104,9 +118,16 @@ export function UserBookDetailPage() {
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete')
setDeleting(false)
setPendingDelete(false)
}
}

// Clear the confirm timer on unmount so a stray state update can't fire
// after navigation away from this page.
useEffect(() => () => {
if (pendingTimeoutRef.current) window.clearTimeout(pendingTimeoutRef.current)
}, [])

if (!isAuthenticated) {
return (
<>
Expand Down Expand Up @@ -281,8 +302,8 @@ export function UserBookDetailPage() {
type="button"
onClick={handleDelete}
disabled={deleting}
className="user-book-detail__delete-icon"
aria-label={deleting ? 'Deleting…' : 'Delete this book'}
className={`user-book-detail__delete-icon${pendingDelete ? ' user-book-detail__delete-icon--confirming' : ''}`}
aria-label={deleting ? 'Deleting…' : pendingDelete ? 'Click again to confirm delete' : 'Delete this book'}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<polyline points="3 6 5 6 21 6" />
Expand All @@ -291,6 +312,7 @@ export function UserBookDetailPage() {
<path d="M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
{pendingDelete && <span className="user-book-detail__delete-icon__confirm-label">Confirm?</span>}
</button>
)}
</div>
Expand Down
40 changes: 31 additions & 9 deletions apps/web/src/styles/books.css
Original file line number Diff line number Diff line change
Expand Up @@ -4662,30 +4662,52 @@ html.dark .add-to-collection-button--icon[aria-label]::after {
background: rgba(0, 0, 0, 0.04);
}

/* Trash icon now lives in the primary action row next to Share/Add-to-collection.
Custom hover tooltip via aria-label ::after. */
/* Trash icon in the primary action row. Idle styling matches the neighbour
icon buttons (Add-to-collection, Link, Share) — neutral border so the
destructive action doesn't shout. Two-stage confirm (see *--confirming
below) handles the danger emphasis only when actually arming the action. */
.user-book-detail__delete-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
gap: 6px;
height: 36px;
border-radius: 50%;
min-width: 36px;
padding: 0 10px;
border-radius: 18px;
background: transparent;
border: 1px solid #dc2626;
color: #dc2626;
border: 1px solid var(--color-border, #E8E2D9);
color: var(--color-text-muted, #6b6b6b);
cursor: pointer;
padding: 0;
position: relative;
transition: background 0.15s ease, border-color 0.15s ease;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.user-book-detail__delete-icon:hover:not(:disabled) {
background: rgba(220, 38, 38, 0.08);
color: #dc2626;
border-color: #dc2626;
background: rgba(220, 38, 38, 0.06);
}
.user-book-detail__delete-icon:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Armed state — second click within 3s actually deletes. Red filled pill
with inline "Confirm?" label, mirrors VocabularyPage's delete pattern. */
.user-book-detail__delete-icon--confirming,
.user-book-detail__delete-icon--confirming:hover:not(:disabled) {
background: #dc2626;
border-color: #dc2626;
color: #fff;
}
.user-book-detail__delete-icon__confirm-label {
font-size: 13px;
font-weight: 600;
line-height: 1;
}
/* Tooltip is redundant once the confirm label is visible — hide it. */
.user-book-detail__delete-icon--confirming[aria-label]::after {
display: none;
}
.user-book-detail__delete-icon[aria-label]::after {
content: attr(aria-label);
position: absolute;
Expand Down
Loading