From 83d0ba26f5eeec3e189d279710d5bbcf13e82f29 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 23 Oct 2025 10:01:39 -0500 Subject: [PATCH] feat(create-room): replace hardcoded game grid with dynamic Radix Select dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the hardcoded 3-game grid with a beautiful, dynamic dropdown that: - Automatically shows all games from getAvailableGames() registry - No more manual updates needed when adding new games - Card Sorting now appears in the modal (was missing before) UI Improvements: - Fancy Radix UI Select component with rich game cards - Each game shows: large emoji icon, title, description, player count, difficulty - Color-coded selection highlights matching game's brand color - Optional game selection - users can "choose later" on game page UX Enhancements: - Smooth CSS scrolling with scroll-behavior: smooth - Absolutely positioned scroll indicators (no jitter) - Green ▲▼ arrows show when more content available - Smart positioning with collision detection (never clips viewport) - maxHeight: 50vh, collisionPadding: 30px for small screens - Hover effects on scroll arrows and game cards Technical: - Uses react-spring animated components for polish - Radix Select for accessibility and keyboard navigation - Single source of truth: game registry manifest data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/components/nav/CreateRoomModal.tsx | 371 ++++++++++++++---- 1 file changed, 304 insertions(+), 67 deletions(-) diff --git a/apps/web/src/components/nav/CreateRoomModal.tsx b/apps/web/src/components/nav/CreateRoomModal.tsx index efde7405..42b2b4a6 100644 --- a/apps/web/src/components/nav/CreateRoomModal.tsx +++ b/apps/web/src/components/nav/CreateRoomModal.tsx @@ -1,6 +1,9 @@ import { useState } from 'react' +import * as Select from '@radix-ui/react-select' +import { animated } from '@react-spring/web' import { Modal } from '@/components/common/Modal' import { useCreateRoom } from '@/hooks/useRoomData' +import { getAvailableGames } from '@/lib/arcade/game-registry' export interface CreateRoomModalProps { /** @@ -24,10 +27,9 @@ export interface CreateRoomModalProps { */ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalProps) { const { mutateAsync: createRoom, isPending } = useCreateRoom() + const availableGames = getAvailableGames() const [error, setError] = useState('') - const [gameName, setGameName] = useState<'matching' | 'memory-quiz' | 'complement-race'>( - 'matching' - ) + const [gameName, setGameName] = useState('__choose_later__') // Special value = user will choose later const [accessMode, setAccessMode] = useState< 'open' | 'password' | 'approval-only' | 'restricted' >('open') @@ -35,7 +37,7 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP const handleClose = () => { setError('') - setGameName('matching') + setGameName('__choose_later__') setAccessMode('open') setPassword('') onClose() @@ -59,9 +61,13 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP try { // Create the room (creator is auto-added as first member) + // If no game selected (choose later), use first available game as default + const selectedGame = + gameName === '__choose_later__' ? availableGames[0]?.manifest.name || 'matching' : gameName + await createRoom({ name, - gameName, + gameName: selectedGame, creatorName: 'Player', gameConfig: { difficulty: 6 }, accessMode, @@ -153,77 +159,308 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP -
- {[ - { value: 'matching' as const, emoji: '🃏', label: 'Memory', desc: 'Matching' }, - { value: 'memory-quiz' as const, emoji: '🧠', label: 'Memory', desc: 'Quiz' }, - { - value: 'complement-race' as const, - emoji: '⚡', - label: 'Complement', - desc: 'Race', - }, - ].map((game) => ( - - ))} -
+ + { + e.currentTarget.style.opacity = '0.7' + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = '1' + }} + > + ▲ + + + + { + if (gameName !== '__choose_later__') { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)' + } + }} + onMouseLeave={(e) => { + if (gameName !== '__choose_later__') { + e.currentTarget.style.background = 'transparent' + } + }} + > + +
+ +
+
Choose later
+
+ Pick on the game selection page +
+
+
+
+
+ +
+ + {availableGames.map((game) => { + const gameId = game.manifest.name + // Map game gradients to colors + const gradientColors: Record = { + pink: 'rgba(236, 72, 153, 0.2)', + purple: 'rgba(168, 85, 247, 0.2)', + blue: 'rgba(59, 130, 246, 0.2)', + green: 'rgba(34, 197, 94, 0.2)', + orange: 'rgba(249, 115, 22, 0.2)', + red: 'rgba(239, 68, 68, 0.2)', + } + const bgColor = + gradientColors[game.manifest.gradient || 'blue'] || gradientColors.blue + + return ( + { + if (gameName !== gameId) { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)' + } + }} + onMouseLeave={(e) => { + if (gameName !== gameId) { + e.currentTarget.style.background = 'transparent' + } + }} + > + +
+ {game.manifest.icon} +
+
+ {game.manifest.displayName} +
+
+ {game.manifest.description} +
+
+ + {game.manifest.maxPlayers === 1 + ? '👤 Solo' + : `👥 ${game.manifest.maxPlayers}p`} + + + {game.manifest.difficulty} + +
+
+
+
+
+ ) + })} + + + { + e.currentTarget.style.opacity = '0.7' + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = '1' + }} + > + ▼ + + + + +
{/* Access Mode Selection */}