Add mobile layout: full-width list with bottom sheet card detail
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -19,9 +19,9 @@ function typeBadge(type) {
|
|||||||
|
|
||||||
const PRINTING_COLS = '80px 1fr 150px 100px';
|
const PRINTING_COLS = '80px 1fr 150px 100px';
|
||||||
|
|
||||||
function CardRow({ card }) {
|
function CardRow({ card, isMobile }) {
|
||||||
const { expandedCardId, setExpandedCardId, ownedAmounts } = useContext(CardContext);
|
const { expandedCardId, setExpandedCardId, ownedAmounts } = useContext(CardContext);
|
||||||
const isExpanded = expandedCardId === card.id;
|
const isExpanded = !isMobile && expandedCardId === card.id;
|
||||||
|
|
||||||
const totalOwned = card.printings?.reduce((sum, p) => {
|
const totalOwned = card.printings?.reduce((sum, p) => {
|
||||||
const key = `${p.set_id}-${p.rarity_id}`;
|
const key = `${p.set_id}-${p.rarity_id}`;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const CHIPS = [
|
|||||||
|
|
||||||
function FilterBar({ typeFilter, setTypeFilter, ownedOnly, setOwnedOnly, sortBy, setSortBy }) {
|
function FilterBar({ typeFilter, setTypeFilter, ownedOnly, setOwnedOnly, sortBy, setSortBy }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', margin: '8px 0' }}>
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', margin: '8px 0', overflowX: 'auto', whiteSpace: 'nowrap', scrollbarWidth: 'none' }}>
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
{CHIPS.map(({ label, activeClass }) => (
|
{CHIPS.map(({ label, activeClass }) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useMediaQuery(query) {
|
||||||
|
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
|
||||||
|
useEffect(() => {
|
||||||
|
const mq = window.matchMedia(query);
|
||||||
|
const handler = e => setMatches(e.matches);
|
||||||
|
mq.addEventListener('change', handler);
|
||||||
|
return () => mq.removeEventListener('change', handler);
|
||||||
|
}, [query]);
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
@@ -92,3 +92,43 @@ body {
|
|||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
.import-modal button:hover { background: #333; }
|
.import-modal button:hover { background: #333; }
|
||||||
|
|
||||||
|
/* Mobile bottom sheet */
|
||||||
|
.sheet-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
z-index: 100;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
.bottom-sheet {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 78vh;
|
||||||
|
background: #161616;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
z-index: 101;
|
||||||
|
overflow-y: auto;
|
||||||
|
animation: slideUp 0.25s ease;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
}
|
||||||
|
.sheet-handle {
|
||||||
|
width: 36px;
|
||||||
|
height: 4px;
|
||||||
|
background: #2e2e2e;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { transform: translateY(100%); }
|
||||||
|
to { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile touch targets */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.icon-btn { width: 30px; height: 30px; font-size: 16px; }
|
||||||
|
.filter-chip { padding: 4px 14px; font-size: 13px; }
|
||||||
|
}
|
||||||
|
|||||||
+101
-62
@@ -3,10 +3,12 @@ import { Virtuoso } from 'react-virtuoso';
|
|||||||
import { CardContext } from '../context/CardContext';
|
import { CardContext } from '../context/CardContext';
|
||||||
import { fetchCards, fetchCardImage } from '../services/api';
|
import { fetchCards, fetchCardImage } from '../services/api';
|
||||||
import { useDebounce } from '../hooks/useDebounce';
|
import { useDebounce } from '../hooks/useDebounce';
|
||||||
|
import { useMediaQuery } from '../hooks/useMediaQuery';
|
||||||
import CardRow from '../components/CardRow/CardRow';
|
import CardRow from '../components/CardRow/CardRow';
|
||||||
import SearchBar from '../components/SearchBar/SearchBar';
|
import SearchBar from '../components/SearchBar/SearchBar';
|
||||||
import FilterBar from '../components/FilterBar/FilterBar';
|
import FilterBar from '../components/FilterBar/FilterBar';
|
||||||
import Footer from '../components/Footer/Footer';
|
import Footer from '../components/Footer/Footer';
|
||||||
|
import PrintingRow from '../components/PrintingRow/PrintingRow';
|
||||||
|
|
||||||
const BADGE = {
|
const BADGE = {
|
||||||
monster: { background: '#3a2000', color: '#d4820a' },
|
monster: { background: '#3a2000', color: '#d4820a' },
|
||||||
@@ -22,6 +24,9 @@ function typeBadge(type) {
|
|||||||
return BADGE.other;
|
return BADGE.other;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MOBILE_PRINTING_COLS = '60px 1fr 100px 80px';
|
||||||
|
const DESKTOP_PRINTING_COLS = '80px 1fr 150px 100px';
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const [cards, setCards] = useState([]);
|
const [cards, setCards] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -33,6 +38,7 @@ function HomePage() {
|
|||||||
|
|
||||||
const { expandedCardId, setExpandedCardId, cardImages, setCardImage, ownedAmounts } = useContext(CardContext);
|
const { expandedCardId, setExpandedCardId, cardImages, setCardImage, ownedAmounts } = useContext(CardContext);
|
||||||
const [artworkIndex, setArtworkIndex] = useState(0);
|
const [artworkIndex, setArtworkIndex] = useState(0);
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
const debouncedSearch = useDebounce(searchTerm, 250);
|
const debouncedSearch = useDebounce(searchTerm, 250);
|
||||||
|
|
||||||
const loadCards = useCallback(() => fetchCards().then(setCards), []);
|
const loadCards = useCallback(() => fetchCards().then(setCards), []);
|
||||||
@@ -48,7 +54,6 @@ function HomePage() {
|
|||||||
const imageIds = cardImages[expandedCardId]?.ids;
|
const imageIds = cardImages[expandedCardId]?.ids;
|
||||||
const imageId = imageIds?.[artworkIndex];
|
const imageId = imageIds?.[artworkIndex];
|
||||||
if (cardImages[expandedCardId]?.blobs[artworkIndex]) return;
|
if (cardImages[expandedCardId]?.blobs[artworkIndex]) return;
|
||||||
// imageId may be undefined on first load (ids not yet fetched)
|
|
||||||
fetchCardImage(expandedCardId, imageId)
|
fetchCardImage(expandedCardId, imageId)
|
||||||
.then(({ image, image_ids }) => setCardImage(expandedCardId, artworkIndex, image, image_ids))
|
.then(({ image, image_ids }) => setCardImage(expandedCardId, artworkIndex, image, image_ids))
|
||||||
.catch(err => console.error('Failed to load card image', err));
|
.catch(err => console.error('Failed to load card image', err));
|
||||||
@@ -61,14 +66,14 @@ function HomePage() {
|
|||||||
}, 0) ?? 0
|
}, 0) ?? 0
|
||||||
, [ownedAmounts]);
|
, [ownedAmounts]);
|
||||||
|
|
||||||
|
const expandedCard = cards.find(c => c.id === expandedCardId);
|
||||||
|
|
||||||
const stats = useMemo(() => ({
|
const stats = useMemo(() => ({
|
||||||
total: cards.length,
|
total: cards.length,
|
||||||
uniqueOwned: cards.filter(c => getTotal(c) > 0).length,
|
uniqueOwned: cards.filter(c => getTotal(c) > 0).length,
|
||||||
totalCopies: cards.reduce((s, c) => s + getTotal(c), 0),
|
totalCopies: cards.reduce((s, c) => s + getTotal(c), 0),
|
||||||
}), [cards, getTotal]);
|
}), [cards, getTotal]);
|
||||||
|
|
||||||
const expandedCard = cards.find(c => c.id === expandedCardId);
|
|
||||||
|
|
||||||
const filteredCards = useMemo(() => {
|
const filteredCards = useMemo(() => {
|
||||||
let result = cards;
|
let result = cards;
|
||||||
if (debouncedSearch) {
|
if (debouncedSearch) {
|
||||||
@@ -80,7 +85,6 @@ function HomePage() {
|
|||||||
result = result.filter(c => c.type.toLowerCase().includes(q));
|
result = result.filter(c => c.type.toLowerCase().includes(q));
|
||||||
}
|
}
|
||||||
if (ownedOnly) result = result.filter(c => getTotal(c) > 0);
|
if (ownedOnly) result = result.filter(c => getTotal(c) > 0);
|
||||||
|
|
||||||
const sorted = [...result];
|
const sorted = [...result];
|
||||||
if (sortBy === 'owned') sorted.sort((a, b) => getTotal(b) - getTotal(a));
|
if (sortBy === 'owned') sorted.sort((a, b) => getTotal(b) - getTotal(a));
|
||||||
else sorted.sort((a, b) => a.name.localeCompare(b.name));
|
else sorted.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
@@ -90,18 +94,88 @@ function HomePage() {
|
|||||||
if (loading) return <p style={{ padding: '1rem', color: '#666' }}>Loading cards…</p>;
|
if (loading) return <p style={{ padding: '1rem', color: '#666' }}>Loading cards…</p>;
|
||||||
if (error) return <p style={{ padding: '1rem', color: '#c04040' }}>Error: {error}</p>;
|
if (error) return <p style={{ padding: '1rem', color: '#c04040' }}>Error: {error}</p>;
|
||||||
|
|
||||||
|
const cardDetailContent = (card) => {
|
||||||
|
if (!card) return null;
|
||||||
|
const imgs = cardImages[card.id];
|
||||||
|
const blob = imgs?.blobs[artworkIndex];
|
||||||
|
const ids = imgs?.ids ?? [];
|
||||||
|
const printingCols = isMobile ? MOBILE_PRINTING_COLS : DESKTOP_PRINTING_COLS;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ position: 'relative', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
{blob
|
||||||
|
? <img src={blob} alt={card.name} style={{ maxWidth: isMobile ? '60%' : '100%' }} />
|
||||||
|
: <div style={{ color: '#444', fontSize: '12px', padding: '2rem' }}>Loading image…</div>
|
||||||
|
}
|
||||||
|
{ids.length > 1 && (
|
||||||
|
<div style={{ position: 'absolute', bottom: '6px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<button className="icon-btn" onClick={() => setArtworkIndex(i => Math.max(0, i - 1))} disabled={artworkIndex === 0}>‹</button>
|
||||||
|
<span style={{ fontSize: '11px', color: '#555' }}>{artworkIndex + 1} / {ids.length}</span>
|
||||||
|
<button className="icon-btn" onClick={() => setArtworkIndex(i => Math.min(ids.length - 1, i + 1))} disabled={artworkIndex === ids.length - 1}>›</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ width: '100%', borderTop: '1px solid #222', paddingTop: '10px', display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||||
|
<span style={{ fontSize: '15px', fontWeight: 600, color: '#e0e0e0' }}>{card.name}</span>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||||
|
<span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '10px', fontWeight: 500, ...typeBadge(card.type) }}>{card.type}</span>
|
||||||
|
{card.attribute && <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '10px', background: '#202020', color: '#888' }}>{card.attribute}</span>}
|
||||||
|
{card.race && <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '10px', background: '#202020', color: '#888' }}>{card.race}</span>}
|
||||||
|
</div>
|
||||||
|
{card.level != null && <span style={{ fontSize: '12px', color: '#888' }}>{'★'.repeat(Math.min(card.level, 13))} Lv.{card.level}</span>}
|
||||||
|
{card.link_val != null && <span style={{ fontSize: '12px', color: '#888' }}>Link {card.link_val}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isMobile && card.printings?.length > 0 && (
|
||||||
|
<div style={{ width: '100%', borderTop: '1px solid #222', paddingTop: '6px' }}>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid', gridTemplateColumns: printingCols,
|
||||||
|
gap: '8px', padding: '5px 12px',
|
||||||
|
fontSize: '10px', color: '#444',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||||
|
borderBottom: '1px solid #222',
|
||||||
|
}}>
|
||||||
|
<span>Code</span><span>Set</span><span>Rarity</span><span>Owned</span>
|
||||||
|
</div>
|
||||||
|
{card.printings.map((printing, i) => (
|
||||||
|
<PrintingRow
|
||||||
|
key={`${card.id}-${printing.set_id}-${printing.rarity_id}`}
|
||||||
|
card_id={card.id}
|
||||||
|
printing={printing}
|
||||||
|
cols={printingCols}
|
||||||
|
zebra={i % 2 === 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
|
||||||
{/* Stats bar */}
|
{/* Stats bar */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: '2rem', padding: '6px 16px', flexShrink: 0,
|
display: 'flex', alignItems: 'center', gap: isMobile ? '1rem' : '2rem',
|
||||||
|
padding: isMobile ? '6px 12px' : '6px 16px', flexShrink: 0,
|
||||||
background: '#1c1c1c', borderBottom: '1px solid #2a2a2a',
|
background: '#1c1c1c', borderBottom: '1px solid #2a2a2a',
|
||||||
fontSize: '12px', color: '#666',
|
fontSize: '12px', color: '#666',
|
||||||
}}>
|
}}>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<span><strong style={{ color: '#e0e0e0' }}>{stats.total.toLocaleString()}</strong> cards</span>
|
||||||
|
<span><strong style={{ color: '#e0e0e0' }}>{stats.uniqueOwned.toLocaleString()}</strong> owned</span>
|
||||||
|
<span><strong style={{ color: '#e0e0e0' }}>{stats.totalCopies.toLocaleString()}</strong> copies</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<span><strong style={{ color: '#e0e0e0' }}>{stats.total.toLocaleString()}</strong> cards in DB</span>
|
<span><strong style={{ color: '#e0e0e0' }}>{stats.total.toLocaleString()}</strong> cards in DB</span>
|
||||||
<span><strong style={{ color: '#e0e0e0' }}>{stats.uniqueOwned.toLocaleString()}</strong> unique owned</span>
|
<span><strong style={{ color: '#e0e0e0' }}>{stats.uniqueOwned.toLocaleString()}</strong> unique owned</span>
|
||||||
<span><strong style={{ color: '#e0e0e0' }}>{stats.totalCopies.toLocaleString()}</strong> total copies</span>
|
<span><strong style={{ color: '#e0e0e0' }}>{stats.totalCopies.toLocaleString()}</strong> total copies</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div style={{ marginLeft: 'auto' }}>
|
<div style={{ marginLeft: 'auto' }}>
|
||||||
<Footer onImportComplete={loadCards} />
|
<Footer onImportComplete={loadCards} />
|
||||||
</div>
|
</div>
|
||||||
@@ -110,9 +184,9 @@ function HomePage() {
|
|||||||
{/* Main panels */}
|
{/* Main panels */}
|
||||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||||
|
|
||||||
{/* Left — card list */}
|
{/* Card list */}
|
||||||
<div style={{ flex: 2, display: 'flex', flexDirection: 'column', borderRight: '1px solid #2a2a2a', overflow: 'hidden' }}>
|
<div style={{ flex: isMobile ? 1 : 2, display: 'flex', flexDirection: 'column', borderRight: isMobile ? 'none' : '1px solid #2a2a2a', overflow: 'hidden' }}>
|
||||||
<div style={{ padding: '10px 16px', borderBottom: '1px solid #2a2a2a', flexShrink: 0 }}>
|
<div style={{ padding: isMobile ? '8px 12px' : '10px 16px', borderBottom: '1px solid #2a2a2a', flexShrink: 0 }}>
|
||||||
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
||||||
<FilterBar
|
<FilterBar
|
||||||
typeFilter={typeFilter} setTypeFilter={setTypeFilter}
|
typeFilter={typeFilter} setTypeFilter={setTypeFilter}
|
||||||
@@ -126,68 +200,33 @@ function HomePage() {
|
|||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
data={filteredCards}
|
data={filteredCards}
|
||||||
itemContent={(_, card) => <CardRow card={card} />}
|
itemContent={(_, card) => <CardRow card={card} isMobile={isMobile} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right — card detail */}
|
{/* Desktop: right panel */}
|
||||||
|
{!isMobile && (
|
||||||
<div style={{ flex: 1, padding: '16px', overflowY: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
|
<div style={{ flex: 1, padding: '16px', overflowY: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
|
||||||
{expandedCard ? (
|
{expandedCard
|
||||||
<>
|
? cardDetailContent(expandedCard)
|
||||||
{(() => {
|
: <span style={{ color: '#333', fontSize: '13px', marginTop: '2rem' }}>Click a card to see details</span>
|
||||||
const imgs = cardImages[expandedCardId];
|
}
|
||||||
const blob = imgs?.blobs[artworkIndex];
|
|
||||||
const ids = imgs?.ids ?? [];
|
|
||||||
return (
|
|
||||||
<div style={{ position: 'relative', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
{blob ? (
|
|
||||||
<img src={blob} alt={expandedCard.name} style={{ maxWidth: '100%' }} />
|
|
||||||
) : (
|
|
||||||
<div style={{ color: '#444', fontSize: '12px', padding: '2rem' }}>Loading image…</div>
|
|
||||||
)}
|
|
||||||
{ids.length > 1 && (
|
|
||||||
<div style={{ position: 'absolute', bottom: '6px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<button className="icon-btn" onClick={() => setArtworkIndex(i => Math.max(0, i - 1))} disabled={artworkIndex === 0}>‹</button>
|
|
||||||
<span style={{ fontSize: '11px', color: '#555' }}>{artworkIndex + 1} / {ids.length}</span>
|
|
||||||
<button className="icon-btn" onClick={() => setArtworkIndex(i => Math.min(ids.length - 1, i + 1))} disabled={artworkIndex === ids.length - 1}>›</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})()}
|
|
||||||
<div style={{ width: '100%', borderTop: '1px solid #222', paddingTop: '10px', display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
|
||||||
<span style={{ fontSize: '15px', fontWeight: 600, color: '#e0e0e0' }}>{expandedCard.name}</span>
|
|
||||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
|
||||||
<span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '10px', fontWeight: 500, ...typeBadge(expandedCard.type) }}>
|
|
||||||
{expandedCard.type}
|
|
||||||
</span>
|
|
||||||
{expandedCard.attribute && (
|
|
||||||
<span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '10px', background: '#202020', color: '#888' }}>
|
|
||||||
{expandedCard.attribute}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{expandedCard.race && (
|
|
||||||
<span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '10px', background: '#202020', color: '#888' }}>
|
|
||||||
{expandedCard.race}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{expandedCard.level != null && (
|
|
||||||
<span style={{ fontSize: '12px', color: '#888' }}>
|
|
||||||
{'★'.repeat(Math.min(expandedCard.level, 13))} Lv.{expandedCard.level}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{expandedCard.link_val != null && (
|
|
||||||
<span style={{ fontSize: '12px', color: '#888' }}>Link {expandedCard.link_val}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: '#333', fontSize: '13px', marginTop: '2rem' }}>Click a card to see details</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Mobile: bottom sheet */}
|
||||||
|
{isMobile && expandedCard && (
|
||||||
|
<>
|
||||||
|
<div className="sheet-backdrop" onClick={() => setExpandedCardId(null)} />
|
||||||
|
<div className="bottom-sheet">
|
||||||
|
<div className="sheet-handle" />
|
||||||
|
<div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||||
|
{cardDetailContent(expandedCard)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user