feat(card-sorting): auto-arrange prefix/suffix cards in corners

Cards in the correct prefix and suffix now automatically arrange themselves:
- Prefix cards: top-left corner, side by side, left to right
- Suffix cards: bottom-right corner, side by side, right to left

**Arrangement details:**
- Cards positioned at 5% margin from edges
- 2% spacing between adjacent cards
- No rotation (perfectly aligned)
- Higher z-index (1000+) so they layer on top
- Smooth spring animation as they move into position

**How it works:**
1. Find prefix cards (positions 0,1,2... all matching correct order)
2. Find suffix cards (positions ...n-2,n-1,n all matching correct order)
3. Calculate positions for organized layout
4. Update card states and sync to server
5. React-spring animates the cards smoothly to their new positions

**Example with correct order [10, 20, 30, 40, 50]:**
- Sequence [10, 20, 50, 30, 40] → 10,20 auto-arrange top-left
- Sequence [30, 10, 20, 40, 50] → 40,50 auto-arrange bottom-right
- Sequence [10, 20, 30, 50, 40] → 10,20,30 auto-arrange top-left

This provides clear visual organization and frees up workspace for
sorting the remaining unsorted cards in the middle section.

🤖 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 21:54:30 -05:00
parent 8f6feec4f2
commit 4ba7f24717

View File

@@ -1225,6 +1225,84 @@ export function PlayingPhaseDrag() {
...state.placedCards.filter((c): c is SortingCard => c !== null),
])
// Auto-arrange correct prefix and suffix cards
useEffect(() => {
if (cardStates.size === 0) return
if (inferredSequence.length === 0) return
if (isSpectating) return // Don't auto-arrange for spectators
const newStates = new Map(cardStates)
let hasChanges = false
// Card dimensions in percentages
const CARD_WIDTH_PCT = 14 // ~140px on ~1000px viewport
const CARD_HEIGHT_PCT = 22.5 // ~180px on ~800px viewport
const SPACING = 2 // spacing between cards
// Find prefix cards (cards at positions 0, 1, 2... that are all correct)
const prefixCards: SortingCard[] = []
for (let i = 0; i < inferredSequence.length; i++) {
if (inferredSequence[i]?.id !== state.correctOrder[i]?.id) break
prefixCards.push(inferredSequence[i])
}
// Find suffix cards (cards at positions ...n-2, n-1, n that are all correct)
const suffixCards: SortingCard[] = []
for (let i = inferredSequence.length - 1; i >= 0; i--) {
const correctIdx = state.correctOrder.length - 1 - (inferredSequence.length - 1 - i)
if (inferredSequence[i]?.id !== state.correctOrder[correctIdx]?.id) break
suffixCards.unshift(inferredSequence[i])
}
// Arrange prefix cards at top-left, side by side
prefixCards.forEach((card, index) => {
const x = 5 + index * (CARD_WIDTH_PCT + SPACING) // Start at 5% margin
const y = 5 // Top margin
const rotation = 0 // No rotation for organized cards
const zIndex = 1000 + index // Higher z-index so they're on top
const currentState = newStates.get(card.id)
if (
currentState &&
(currentState.x !== x || currentState.y !== y || currentState.rotation !== rotation)
) {
newStates.set(card.id, { x, y, rotation, zIndex })
hasChanges = true
}
})
// Arrange suffix cards at bottom-right, side by side (right to left)
suffixCards.forEach((card, index) => {
const fromRight = suffixCards.length - 1 - index
const x = 100 - CARD_WIDTH_PCT - 5 - fromRight * (CARD_WIDTH_PCT + SPACING) // From right edge
const y = 100 - CARD_HEIGHT_PCT - 5 // Bottom margin
const rotation = 0 // No rotation for organized cards
const zIndex = 1000 + index // Higher z-index so they're on top
const currentState = newStates.get(card.id)
if (
currentState &&
(currentState.x !== x || currentState.y !== y || currentState.rotation !== rotation)
) {
newStates.set(card.id, { x, y, rotation, zIndex })
hasChanges = true
}
})
if (hasChanges) {
setCardStates(newStates)
// Send updated positions to server
const positions = Array.from(newStates.entries()).map(([id, cardState]) => ({
cardId: id,
x: cardState.x,
y: cardState.y,
rotation: cardState.rotation,
zIndex: cardState.zIndex,
}))
updateCardPositions(positions)
}
}, [inferredSequence, state.correctOrder, cardStates, isSpectating, updateCardPositions])
// Format time display
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60)