feat(card-sorting): add collapsible stats sidebar for spectators

Add comprehensive real-time statistics sidebar for spectators with smooth
collapse/expand functionality and color-coded metrics.

Features:
- Collapsible sidebar (280px width, slides in from right)
- Collapse toggle button with smooth animations
- Real-time statistics cards with gradient backgrounds:
  - Time Elapsed (blue gradient, MM:SS format)
  - Cards Placed (green gradient, with completion percentage)
  - Current Accuracy (yellow gradient, % of correctly positioned cards)
  - Numbers Revealed status (pink/gray gradient based on state)
- Each stat card has emoji icons and descriptive labels
- Smooth slide-in/out transition (0.3s ease)
- Fixed position below spectator banner
- Z-index 90 (below banner which is 100)

UI Details:
- Sidebar positioned at top: 56px (below banner)
- Right slide animation from -280px to 0
- Toggle button extends slightly on hover (40px → 44px)
- Arrow indicators (◀ when collapsed, ▶ when expanded)
- Semi-transparent white background (95% opacity)
- Subtle box shadow for depth
- Scrollable content area for long stat lists

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-10-23 17:21:08 -05:00
parent ee7345d641
commit 6527c26a81
1 changed files with 176 additions and 0 deletions

View File

@ -826,6 +826,8 @@ export function PlayingPhaseDrag() {
// Spectator educational mode (show correctness indicators)
const [spectatorEducationalMode, setSpectatorEducationalMode] = useState(false)
// Spectator stats sidebar collapsed state
const [spectatorStatsCollapsed, setSpectatorStatsCollapsed] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const dragStateRef = useRef<{
@ -1343,6 +1345,180 @@ export function PlayingPhaseDrag() {
</div>
)}
{/* Spectator Stats Sidebar */}
{isSpectating && (
<div
className={css({
position: 'fixed',
top: '56px', // Below banner
right: spectatorStatsCollapsed ? '-280px' : '0',
width: '280px',
height: 'calc(100vh - 56px)',
background: 'rgba(255, 255, 255, 0.95)',
boxShadow: '-2px 0 12px rgba(0, 0, 0, 0.1)',
transition: 'right 0.3s ease',
zIndex: 90,
display: 'flex',
flexDirection: 'column',
})}
>
{/* Collapse/Expand Toggle */}
<button
type="button"
onClick={() => setSpectatorStatsCollapsed(!spectatorStatsCollapsed)}
className={css({
position: 'absolute',
left: '-40px',
top: '50%',
transform: 'translateY(-50%)',
width: '40px',
height: '80px',
background: 'rgba(255, 255, 255, 0.95)',
border: 'none',
borderRadius: '8px 0 0 8px',
boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.1)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
transition: 'all 0.2s',
_hover: {
background: 'rgba(255, 255, 255, 1)',
width: '44px',
},
})}
>
{spectatorStatsCollapsed ? '◀' : '▶'}
</button>
{/* Stats Content */}
<div
className={css({
padding: '24px',
overflowY: 'auto',
flex: 1,
})}
>
<h3
className={css({
fontSize: '18px',
fontWeight: '700',
marginBottom: '20px',
color: '#1e293b',
borderBottom: '2px solid #e2e8f0',
paddingBottom: '8px',
})}
>
📊 Live Stats
</h3>
{/* Time Elapsed */}
<div
className={css({
marginBottom: '16px',
padding: '12px',
background: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
borderRadius: '8px',
border: '1px solid #93c5fd',
})}
>
<div className={css({ fontSize: '12px', color: '#1e40af', marginBottom: '4px' })}>
Time Elapsed
</div>
<div className={css({ fontSize: '24px', fontWeight: '700', color: '#1e3a8a' })}>
{Math.floor(elapsedTime / 60)}:{(elapsedTime % 60).toString().padStart(2, '0')}
</div>
</div>
{/* Cards Placed */}
<div
className={css({
marginBottom: '16px',
padding: '12px',
background: 'linear-gradient(135deg, #dcfce7, #bbf7d0)',
borderRadius: '8px',
border: '1px solid #86efac',
})}
>
<div className={css({ fontSize: '12px', color: '#15803d', marginBottom: '4px' })}>
🎯 Cards Placed
</div>
<div className={css({ fontSize: '24px', fontWeight: '700', color: '#14532d' })}>
{state.placedCards.filter((c) => c !== null).length} / {state.cardCount}
</div>
<div className={css({ fontSize: '11px', color: '#15803d', marginTop: '4px' })}>
{Math.round(
(state.placedCards.filter((c) => c !== null).length / state.cardCount) * 100
)}
% complete
</div>
</div>
{/* Current Accuracy */}
<div
className={css({
marginBottom: '16px',
padding: '12px',
background: 'linear-gradient(135deg, #fef3c7, #fde68a)',
borderRadius: '8px',
border: '1px solid #fbbf24',
})}
>
<div className={css({ fontSize: '12px', color: '#92400e', marginBottom: '4px' })}>
Current Accuracy
</div>
<div className={css({ fontSize: '24px', fontWeight: '700', color: '#78350f' })}>
{(() => {
const placedCards = state.placedCards.filter((c): c is SortingCard => c !== null)
if (placedCards.length === 0) return '0%'
const correctCount = placedCards.filter(
(c, i) => state.correctOrder[i]?.id === c.id
).length
return `${Math.round((correctCount / placedCards.length) * 100)}%`
})()}
</div>
<div className={css({ fontSize: '11px', color: '#92400e', marginTop: '4px' })}>
Cards in correct position
</div>
</div>
{/* Numbers Revealed */}
<div
className={css({
marginBottom: '16px',
padding: '12px',
background: state.numbersRevealed
? 'linear-gradient(135deg, #fce7f3, #fbcfe8)'
: 'linear-gradient(135deg, #f1f5f9, #e2e8f0)',
borderRadius: '8px',
border: '1px solid',
borderColor: state.numbersRevealed ? '#f9a8d4' : '#cbd5e1',
})}
>
<div
className={css({
fontSize: '12px',
color: state.numbersRevealed ? '#9f1239' : '#475569',
marginBottom: '4px',
})}
>
👁 Numbers Revealed
</div>
<div
className={css({
fontSize: '20px',
fontWeight: '600',
color: state.numbersRevealed ? '#9f1239' : '#475569',
})}
>
{state.numbersRevealed ? '✓ Yes' : '✗ No'}
</div>
</div>
</div>
</div>
)}
{/* Floating action buttons */}
{!isSpectating && (
<div