fix: a link on a card previews, and the deck reads like the player
The preview card was there all along and invisible. It was positioned inside the paragraph it belonged to, and a flashcard is a small box with its overflow hidden — so the card was drawn and clipped, and the link read as doing nothing. I had "fixed" that by turning previews off on cards, which was the wrong end of the problem: the preview is what the link is for. It is drawn into the body now, placed from the link's own rectangle, so nothing between the two can clip it. That is a fix everywhere, not only on cards. Two things behind it: - **Clicking a link flipped the card back**, because the whole card is a flip target and the click bubbled — so the preview opened and the thing it was anchored to vanished in the same gesture. Only the empty parts of a card flip it now. - **There was nowhere for "Split view" to go** from a card. The deck now has a pane of its own: the article opens beside the card, the deck keeps its place, and following a reference out of the article replaces the pane rather than losing you. **The deck is boxed like the session player.** Header at the top, verdict at the foot, and only the card scrolls between them. A card with a picture on it is taller than the window, and the two buttons the whole exercise turns on were below the fold: you scrolled down to read the answer and back up to say whether you knew it. Unflipped, the foot offers Show answer rather than two buttons about a card you have not seen. And the four grey pills down the side of every row in Browse are one strip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
6305d82e95
commit
0a2f7d08db
7 changed files with 205 additions and 44 deletions
|
|
@ -13,6 +13,9 @@
|
|||
z-index: 40;
|
||||
left: 0;
|
||||
top: calc(100% + 8px);
|
||||
/* `is-floating` is the card drawn into the body: fixed to the window and
|
||||
placed from the link's rectangle, so nothing between the two can clip it.
|
||||
Above everything, because it is summoned deliberately and briefly. */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
|
@ -45,6 +48,8 @@
|
|||
}
|
||||
.al-card.is-above::before { top: auto; bottom: -9px; }
|
||||
.al-card.is-above { top: auto; bottom: calc(100% + 8px); }
|
||||
.al-card.is-floating { position: fixed; z-index: 1290; width: min(340px, calc(100vw - 16px)); }
|
||||
.al-card.is-floating.is-above { top: auto; }
|
||||
.al-card-title { font-weight: 700; font-size: 0.92rem; color: var(--text); }
|
||||
.al-card-excerpt { color: var(--text-muted); }
|
||||
.al-card-meta { font-size: 0.75rem; color: var(--text-subtle); }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import api from '../api/client'
|
||||
import { useSplitView } from '../context/SplitViewContext'
|
||||
|
|
@ -13,6 +14,11 @@ const HOVER_DELAY = 350 // Long enough that crossing a link does not summon a
|
|||
// Long enough to cross the gap between the link and the card's controls, and to
|
||||
// cover the blur-then-focus gap when Tab moves between them.
|
||||
const HIDE_DELAY = 260
|
||||
//: What the card is, roughly, for deciding which side of the link it fits on
|
||||
//: and how far in from the edge it must sit. Matched to ArticleLink.css.
|
||||
const CARD_WIDTH = 340
|
||||
const CARD_HEIGHT = 220
|
||||
const GAP = 8
|
||||
|
||||
export function fetchPreview(slug) {
|
||||
if (!cache.has(slug)) {
|
||||
|
|
@ -42,6 +48,9 @@ export default function ArticleLink({ slug, sectionId = null, children, classNam
|
|||
const [preview, setPreview] = useState(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [above, setAbove] = useState(false)
|
||||
//: Where the card goes, in window coordinates. It is drawn into the body
|
||||
//: rather than beside the link, so it needs to be told.
|
||||
const [at, setAt] = useState(null)
|
||||
const timer = useRef(null)
|
||||
const anchor = useRef(null)
|
||||
const split = useSplitView()
|
||||
|
|
@ -58,9 +67,20 @@ export default function ArticleLink({ slug, sectionId = null, children, classNam
|
|||
const reveal = useCallback(async () => {
|
||||
const data = await fetchPreview(slug)
|
||||
if (!data) return
|
||||
// Flip the card above the link when there is no room beneath it.
|
||||
const box = anchor.current?.getBoundingClientRect?.()
|
||||
if (box) setAbove(window.innerHeight - box.bottom < 220)
|
||||
if (box) {
|
||||
// Flip the card above the link when there is no room beneath it, and
|
||||
// keep it inside the window sideways.
|
||||
const above = window.innerHeight - box.bottom < CARD_HEIGHT
|
||||
setAbove(above)
|
||||
const left = Math.min(Math.max(8, box.left),
|
||||
Math.max(8, window.innerWidth - CARD_WIDTH - 8))
|
||||
setAt({
|
||||
left,
|
||||
top: above ? undefined : box.bottom + GAP,
|
||||
bottom: above ? window.innerHeight - box.top + GAP : undefined,
|
||||
})
|
||||
}
|
||||
setPreview(data)
|
||||
setOpen(true)
|
||||
}, [slug])
|
||||
|
|
@ -133,8 +153,17 @@ export default function ArticleLink({ slug, sectionId = null, children, classNam
|
|||
<Link ref={anchor} to={href} className={`al-link ${className}`} onClick={followLink}>
|
||||
{children}
|
||||
</Link>
|
||||
{open && preview && (
|
||||
<span className={`al-card${above ? ' is-above' : ''}`} role="tooltip"
|
||||
{open && preview && createPortal((
|
||||
/* Drawn into the body, not beside the link.
|
||||
*
|
||||
* A card positioned inside the paragraph it belongs to is clipped by
|
||||
* whatever that paragraph is inside — and a flashcard is a small box
|
||||
* with its overflow hidden, so the preview was there and invisible,
|
||||
* which reads as a link that does nothing. In the body it cannot be
|
||||
* clipped by anything, and it is placed from the link's own rectangle.
|
||||
*/
|
||||
<span className={`al-card is-floating${above ? ' is-above' : ''}`} role="tooltip"
|
||||
style={at ? { left: at.left, top: at.top, bottom: at.bottom } : undefined}
|
||||
onMouseEnter={hideCancel} onMouseLeave={hideSoon}>
|
||||
<span className="al-card-title">{preview.title}</span>
|
||||
{preview.excerpt && <span className="al-card-excerpt">{preview.excerpt}</span>}
|
||||
|
|
@ -158,7 +187,7 @@ export default function ArticleLink({ slug, sectionId = null, children, classNam
|
|||
</a>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
), document.body)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,13 +68,10 @@ export default function RichText({
|
|||
attemptId,
|
||||
className = '',
|
||||
linkArticles = false,
|
||||
// Whether a cross-reference shows a preview card or simply goes.
|
||||
//
|
||||
// A card is not a page: it is a box a few lines tall, often inside a
|
||||
// flipping panel, and a hover card anchored inside one has nowhere to open —
|
||||
// it is drawn off the edge or clipped by the box, and the link reads as
|
||||
// broken because clicking it appears to do nothing. Where there is no room
|
||||
// to preview, the honest behaviour is to take the reader there.
|
||||
// Whether a cross-reference shows a preview card or simply goes. The card is
|
||||
// drawn into the body now, so nothing can clip it and there is no longer a
|
||||
// place where the preview cannot be shown — this stays for a caller that
|
||||
// genuinely wants a plain link.
|
||||
previewLinks = true,
|
||||
// Passing a textId turns on the highlight layer: text keeps the source
|
||||
// offsets that highlights and the read-aloud cursor are stored against.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import api from '../api/client'
|
||||
import RichText from '../components/RichText'
|
||||
import ArticleSplitPane from '../components/ArticleSplitPane'
|
||||
import { SplitViewProvider } from '../context/SplitViewContext'
|
||||
import ImageFigure from '../components/ImageFigure'
|
||||
|
||||
export default function FlashcardStudyPage() {
|
||||
|
|
@ -110,13 +112,25 @@ export default function FlashcardStudyPage() {
|
|||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [next, prev, currentCard])
|
||||
|
||||
const [splitTrail, setSplitTrail] = useState([])
|
||||
const pushSplit = useCallback((slug) => setSplitTrail(
|
||||
trail => (trail.at(-1) === slug ? trail : [...trail, slug])), [])
|
||||
const splitView = useMemo(() => ({ open: pushSplit, inPane: false }), [pushSplit])
|
||||
const paneView = useMemo(() => ({ open: pushSplit, inPane: true }), [pushSplit])
|
||||
const splitSlug = splitTrail.at(-1) || null
|
||||
|
||||
if (loading) return <div className="loading"><div className="spinner" /></div>
|
||||
if (!deck) return null
|
||||
|
||||
const allDone = known.size === cards.length
|
||||
|
||||
return (
|
||||
<div>
|
||||
/* A card can point at the article it came from, so the article can open
|
||||
beside it — reading the paragraph behind a card is the natural next
|
||||
thing to do, and it should not cost you your place in the deck. */
|
||||
<SplitViewProvider value={splitView}>
|
||||
<div className={splitSlug ? 'fc-study has-split' : 'fc-study'}>
|
||||
<div className="fc-study-main">
|
||||
{/* The way out, above the title and in the same words as everywhere
|
||||
else in the app — not a grey button at the end of a row of controls
|
||||
that change what you are studying. */}
|
||||
|
|
@ -167,15 +181,15 @@ export default function FlashcardStudyPage() {
|
|||
{!allDone && total > 0 && currentCard && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--card-bg)', borderRadius: 'var(--card-radius)',
|
||||
padding: 40, minHeight: 250, display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center', textAlign: 'center',
|
||||
cursor: 'pointer', border: `2px solid ${flipped ? 'var(--primary)' : 'var(--border)'}`,
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.08)', transition: 'border-color 0.2s',
|
||||
marginBottom: 16,
|
||||
className={`fc-card${flipped ? ' is-flipped' : ''}`}
|
||||
/* Anything inside the card that is itself clickable — a
|
||||
cross-reference, a figure — handles its own click; only the
|
||||
empty parts of the card flip it. Following a link used to flip
|
||||
the card back, which closed the very thing being opened. */
|
||||
onClick={event => {
|
||||
if (event.target.closest('a, button, .imgfig, .al-wrap')) return
|
||||
setFlipped(v => !v)
|
||||
}}
|
||||
onClick={() => setFlipped(v => !v)}
|
||||
>
|
||||
<div style={{ fontSize: '0.72rem', color: 'var(--text-muted)', marginBottom: 8, textTransform: 'uppercase', fontWeight: 700, letterSpacing: '0.05em' }}>
|
||||
{flipped ? 'Back' : 'Front'} · Card {currentIdx + 1} of {total}
|
||||
|
|
@ -185,7 +199,7 @@ export default function FlashcardStudyPage() {
|
|||
everything else here, so `[[264|respiratory failure]]`,
|
||||
`==key points==` and a figure all work on a card — which is
|
||||
most of what "link cards to things" turns out to mean. */}
|
||||
<RichText value={flipped ? currentCard.back : currentCard.front} linkArticles previewLinks={false} />
|
||||
<RichText value={flipped ? currentCard.back : currentCard.front} linkArticles />
|
||||
</p>
|
||||
{/* The picture on the card. A card could carry one — the column is
|
||||
there, the editor accepts one, the API returns it — and no view
|
||||
|
|
@ -202,19 +216,25 @@ export default function FlashcardStudyPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-secondary" onClick={prev} disabled={currentIdx === 0}>← Prev</button>
|
||||
{flipped && (
|
||||
<>
|
||||
<button className="btn btn-primary" onClick={markKnown} style={{ background: '#22c55e', borderColor: '#22c55e' }}>Got it ✓</button>
|
||||
<button className="btn btn-secondary" onClick={markReview} style={{ color: '#ef4444', borderColor: '#ef4444' }}>Review again</button>
|
||||
</>
|
||||
)}
|
||||
<button className="btn btn-secondary" onClick={next} disabled={currentIdx >= total - 1}>Next →</button>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: 8, fontSize: '0.75rem', color: 'var(--text-muted)' }}>
|
||||
Keyboard: Space/Enter=flip, ←→=navigate, 1=got it, 2=review
|
||||
{/* The verdict bar, pinned to the foot of the deck the way the
|
||||
player's is. A card with a picture on it is taller than the
|
||||
window, and the two buttons the whole exercise turns on were
|
||||
below the fold — you scrolled to read, then scrolled back to say
|
||||
whether you knew it. */}
|
||||
<div className="fc-foot">
|
||||
<div className="fc-foot-row">
|
||||
<button className="btn btn-secondary" onClick={prev} disabled={currentIdx === 0}>← Prev</button>
|
||||
{flipped ? (
|
||||
<>
|
||||
<button className="btn fc-known" onClick={markKnown}>Got it ✓</button>
|
||||
<button className="btn fc-again" onClick={markReview}>Review again</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-primary" onClick={() => setFlipped(true)}>Show answer</button>
|
||||
)}
|
||||
<button className="btn btn-secondary" onClick={next} disabled={currentIdx >= total - 1}>Next →</button>
|
||||
</div>
|
||||
<p className="fc-keys">Space or Enter flips · ← → moves · 1 got it · 2 review</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -222,6 +242,16 @@ export default function FlashcardStudyPage() {
|
|||
{!allDone && total === 0 && (
|
||||
<div className="card"><div className="empty-state">No cards to show. {mode === 'review' ? 'No cards marked for review.' : ''}</div></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{splitSlug && (
|
||||
<SplitViewProvider value={paneView}>
|
||||
<ArticleSplitPane slug={splitSlug} depth={splitTrail.length - 1}
|
||||
onBack={() => setSplitTrail(trail => trail.slice(0, -1))}
|
||||
onClose={() => setSplitTrail([])} />
|
||||
</SplitViewProvider>
|
||||
)}
|
||||
</div>
|
||||
</SplitViewProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,3 +69,32 @@ it('renders a card face as prose, so a card can carry a link', async () => {
|
|||
expect(within(back.closest('.rich-text')).getByText('C1 inhibitor').tagName).toBe('STRONG')
|
||||
expect(screen.getByText('respiratory failure')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('previews a link on a card, and opens the article beside the deck', async () => {
|
||||
// A card is a small box with its overflow hidden, so a preview positioned
|
||||
// inside it was invisible and the link read as doing nothing. The card is
|
||||
// drawn into the body now, and there is somewhere for Split view to go.
|
||||
api.get.mockImplementation(url => {
|
||||
if (url === '/articles/preview/264') return Promise.resolve({ data: {
|
||||
id: 264, slug: 'respiratory-failure', title: 'Pediatric Respiratory Failure',
|
||||
excerpt: 'Oxygenation or CO2 clearance that no longer meets demand.',
|
||||
section_count: 18, status: 'published' } })
|
||||
if (url === '/articles/264') return Promise.resolve({ data: {
|
||||
id: 264, title: 'Pediatric Respiratory Failure', sections: [], status: 'published' } })
|
||||
if (url === '/flashcards/3/study') return Promise.resolve({ data: DECK })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
mount()
|
||||
await screen.findByText('Hereditary angioedema')
|
||||
await userEvent.click(screen.getByText('Hereditary angioedema'))
|
||||
|
||||
const link = await screen.findByText('respiratory failure')
|
||||
await userEvent.click(link)
|
||||
await waitFor(() => expect(api.get).toHaveBeenCalledWith('/articles/preview/264'))
|
||||
const card = await screen.findByRole('tooltip')
|
||||
expect(within(card).getByRole('link', { name: /new tab/i }))
|
||||
.toHaveAttribute('href', '/articles/264')
|
||||
|
||||
await userEvent.click(within(card).getByRole('button', { name: /split view/i }))
|
||||
expect(await screen.findByRole('region', { name: /Split view/ })).toBeInTheDocument()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,3 +7,71 @@
|
|||
.fc-edit-hint { margin: 0; font-size: .76rem; line-height: 1.6; color: var(--text-muted); }
|
||||
.fc-edit-hint code { font-size: .74rem; }
|
||||
.fc-edit-image { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
|
||||
|
||||
/* Studying a deck with an article open beside it. The deck keeps the middle,
|
||||
the article takes the right, and the deck does not reflow as you follow one
|
||||
reference after another. */
|
||||
.fc-study.has-split {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 40%);
|
||||
gap: 16px; align-items: start;
|
||||
}
|
||||
.fc-study.has-split .article-split-pane { position: sticky; top: 12px; max-height: calc(100dvh - 24px); }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
/* Not enough width for both: the article takes the screen, and closing it
|
||||
puts the card back. */
|
||||
.fc-study.has-split { grid-template-columns: minmax(0, 1fr); }
|
||||
.fc-study.has-split .fc-study-main { display: none; }
|
||||
}
|
||||
|
||||
/* ── Studying a deck ──────────────────────────────────────────────────
|
||||
Boxed, like the session player: the deck's header at the top, the verdict at
|
||||
the foot, and only the card between them scrolls. A card with a picture on
|
||||
it is taller than the window, and the two buttons the whole exercise turns
|
||||
on were below the fold. */
|
||||
.fc-study-main { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
|
||||
|
||||
.fc-card {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 10px; padding: 32px 28px; text-align: center; cursor: pointer;
|
||||
background: var(--card-bg); border: 2px solid var(--border);
|
||||
border-radius: var(--card-radius); box-shadow: 0 4px 20px rgba(0, 0, 0, .08);
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.fc-card.is-flipped { border-color: var(--primary); }
|
||||
.fc-card .imgfig { margin: 0; }
|
||||
|
||||
.fc-foot {
|
||||
position: sticky; bottom: 0; z-index: 2;
|
||||
padding: 10px 0 6px; background: var(--bg);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.fc-foot-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
|
||||
.fc-keys { margin: 8px 0 0; text-align: center; font-size: .74rem; color: var(--text-muted); }
|
||||
|
||||
/* Named rather than styled inline: green for the one that means "done with
|
||||
this", red-lettered for the one that means "again". */
|
||||
.fc-known { background: #22c55e; border-color: #22c55e; color: #fff; }
|
||||
.fc-known:hover { background: #16a34a; border-color: #16a34a; }
|
||||
.fc-again { color: #ef4444; border-color: #ef4444; background: none; }
|
||||
.fc-again:hover { background: #fef2f2; }
|
||||
|
||||
@media (min-width: 900px) {
|
||||
/* Tall enough to be worth boxing: the header and the verdict stay put and
|
||||
the card scrolls between them. Below this the page simply scrolls. */
|
||||
.fc-study-main { height: calc(100dvh - 150px); }
|
||||
}
|
||||
|
||||
/* What you can do to one card in the browse list: one strip, wrapping onto a
|
||||
second line on a narrow screen rather than becoming a column. */
|
||||
.fc-row-actions { display: flex; gap: 6px; flex-wrap: wrap; flex-shrink: 0; align-items: flex-start; }
|
||||
.fc-row-actions .btn { white-space: nowrap; }
|
||||
.fc-row-delete { color: var(--text-muted); }
|
||||
.fc-row-delete:hover { color: var(--wrong-fg, #b91c1c); border-color: currentColor; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
/* Under the card rather than beside it: at 390px a strip of four buttons
|
||||
beside the text leaves the stem two words wide. */
|
||||
.fc-row-actions { width: 100%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ export default function FlashcardsPage() {
|
|||
{/* Rendered, not printed: a card's faces are prose, so a
|
||||
cross-reference on one is a link here too rather than a pair
|
||||
of brackets and a number. */}
|
||||
<RichText value={flipped ? studyCard.back : studyCard.front} linkArticles previewLinks={false} />
|
||||
<RichText value={flipped ? studyCard.back : studyCard.front} linkArticles />
|
||||
{studyCard.image_path && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<ImageFigure src={studyCard.image_path} alt={studyCard.front} />
|
||||
|
|
@ -500,21 +500,24 @@ export default function FlashcardsPage() {
|
|||
<p style={{ fontSize: '0.9rem', fontWeight: 600, marginBottom: 4 }}>{card.front}</p>
|
||||
<p style={{ fontSize: '0.82rem', color: 'var(--text-muted)' }}>{card.back.slice(0, 120)}{card.back.length > 120 ? '...' : ''}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, flexShrink: 0 }}>
|
||||
{/* One row, not a column of four grey pills down the side of
|
||||
every card. They are all one kind of thing — what you can do
|
||||
to this card — so they read as one control strip. */}
|
||||
<div className="fc-row-actions">
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => { setStudyCard(card); setFlipped(false) }}>View</button>
|
||||
{deletingCard === card.id ? (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button className="btn btn-sm btn-danger" onClick={() => deleteCard(card.id)}>Yes</button>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => setDeletingCard(null)}>No</button>
|
||||
</div>
|
||||
<>
|
||||
<button className="btn btn-sm btn-danger" onClick={() => deleteCard(card.id)}>Delete it</button>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => setDeletingCard(null)}>Keep</button>
|
||||
</>
|
||||
) : educator ? (
|
||||
<>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => startEdit(card)}
|
||||
aria-expanded={editCard?.id === card.id}>Edit</button>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => openLinks(card)}
|
||||
aria-expanded={linkCardId === card.id}>Links</button>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => setDeletingCard(card.id)}
|
||||
style={{ fontSize: '0.72rem', color: 'var(--text-muted)' }}>Delete</button>
|
||||
<button className="btn btn-sm btn-secondary fc-row-delete"
|
||||
onClick={() => setDeletingCard(card.id)}>Delete</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue