From 646a4228d0573796b1a429e31bc037411024c0ff Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 23 Oct 2025 10:30:47 -0500 Subject: [PATCH] fix(qr-button): improve layout and z-index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stack room code and share link vertically on left - Place square QR button on right, spanning both rows - Show mini QR code (40px) in button instead of emoji - Fix popover z-index to appear above dropdown menu (z: 10000) - Reduce button padding for more compact appearance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .mcp.json | 16 + apps/web/.claude/CLAUDE.md | 17 + apps/web/.claude/settings.local.json | 12 +- .../src/components/common/QRCodeButton.tsx | 17 +- .../src/components/nav/CreateRoomModal.tsx | 1133 ++++++++++------- .../src/components/nav/RoomShareButtons.tsx | 52 +- pnpm-lock.yaml | 12 + 7 files changed, 732 insertions(+), 527 deletions(-) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..b41341fb --- /dev/null +++ b/.mcp.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "sqlite": { + "command": "/Users/antialias/.nvm/versions/node/v20.19.3/bin/npx", + "args": [ + "-y", + "mcp-server-sqlite-npx", + "/Users/antialias/projects/soroban-abacus-flashcards/apps/web/data/sqlite.db" + ], + "env": { + "PATH": "/Users/antialias/.nvm/versions/node/v20.19.3/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin", + "NODE_PATH": "/Users/antialias/.nvm/versions/node/v20.19.3/lib/node_modules" + } + } + } +} diff --git a/apps/web/.claude/CLAUDE.md b/apps/web/.claude/CLAUDE.md index d78cdf39..3c4a1fd0 100644 --- a/apps/web/.claude/CLAUDE.md +++ b/apps/web/.claude/CLAUDE.md @@ -267,3 +267,20 @@ Before setting a z-index, always check: 1. What stacking context is this element in? 2. Am I comparing against siblings or global elements? 3. Does my parent create a stacking context? + +## Database Access + +This project uses SQLite with Drizzle ORM. Database location: `./data/sqlite.db` + +**ALWAYS use MCP SQLite tools for database operations:** +- `mcp__sqlite__list_tables` - List all tables +- `mcp__sqlite__describe_table` - Get table schema +- `mcp__sqlite__read_query` - Run SELECT queries +- `mcp__sqlite__write_query` - Run INSERT/UPDATE/DELETE queries +- `mcp__sqlite__create_table` - Create new tables +- **DO NOT use bash `sqlite3` commands** - use the MCP tools instead + +**Database Schema:** +- Schema definitions: `src/db/schema/` +- Drizzle config: `drizzle.config.ts` +- Migrations: `drizzle/` directory diff --git a/apps/web/.claude/settings.local.json b/apps/web/.claude/settings.local.json index 1b453488..29c13446 100644 --- a/apps/web/.claude/settings.local.json +++ b/apps/web/.claude/settings.local.json @@ -104,9 +104,17 @@ "Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')", "Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')", "Bash(git rev-parse HEAD)", - "Bash(gh run watch --exit-status 18662351595)" + "Bash(gh run watch --exit-status 18662351595)", + "WebFetch(domain:github.com)", + "WebSearch", + "WebFetch(domain:www.npmjs.com)", + "mcp__sqlite__list_tables", + "mcp__sqlite__describe_table", + "mcp__sqlite__read_query" ], "deny": [], "ask": [] - } + }, + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": ["sqlite"] } diff --git a/apps/web/src/components/common/QRCodeButton.tsx b/apps/web/src/components/common/QRCodeButton.tsx index ab25ebab..3e8d3dfa 100644 --- a/apps/web/src/components/common/QRCodeButton.tsx +++ b/apps/web/src/components/common/QRCodeButton.tsx @@ -3,6 +3,7 @@ import type { CSSProperties } from 'react' import { useState } from 'react' import { QRCodeSVG } from 'qrcode.react' import { useClipboard } from '@/hooks/useClipboard' +import { Z_INDEX } from '@/constants/zIndex' export interface QRCodeButtonProps { /** @@ -25,21 +26,20 @@ export function QRCodeButton({ url, style }: QRCodeButtonProps) { const { copied, copy } = useClipboard() const buttonStyles: CSSProperties = { - width: '100%', cursor: 'pointer', transition: 'all 0.2s ease', display: 'flex', alignItems: 'center', justifyContent: 'center', - gap: '8px', - marginBottom: '6px', border: '2px solid rgba(251, 146, 60, 0.4)', background: 'linear-gradient(135deg, rgba(251, 146, 60, 0.2), rgba(251, 146, 60, 0.3))', borderRadius: '8px', - padding: '10px 16px', - fontSize: '13px', - fontWeight: '600', + padding: '6px', + fontSize: '16px', color: 'rgba(253, 186, 116, 1)', + aspectRatio: '1', + alignSelf: 'stretch', + flexShrink: 0, ...style, } @@ -61,8 +61,7 @@ export function QRCodeButton({ url, style }: QRCodeButtonProps) { Object.assign(e.currentTarget.style, buttonStyles) }} > - 📱 - QR Code + @@ -77,7 +76,7 @@ export function QRCodeButton({ url, style }: QRCodeButtonProps) { borderRadius: '12px', padding: '20px', boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)', - zIndex: 1000, + zIndex: Z_INDEX.GAME_NAV.HAMBURGER_NESTED_DROPDOWN, maxWidth: '320px', }} > diff --git a/apps/web/src/components/nav/CreateRoomModal.tsx b/apps/web/src/components/nav/CreateRoomModal.tsx index 42b2b4a6..7116a3e9 100644 --- a/apps/web/src/components/nav/CreateRoomModal.tsx +++ b/apps/web/src/components/nav/CreateRoomModal.tsx @@ -2,8 +2,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 { useCreateRoom, useRoomData } from '@/hooks/useRoomData' import { getAvailableGames } from '@/lib/arcade/game-registry' +import { RoomShareButtons } from './RoomShareButtons' export interface CreateRoomModalProps { /** @@ -22,11 +23,14 @@ export interface CreateRoomModalProps { onSuccess?: () => void } +type ModalState = 'creating' | 'created' + /** * Modal for creating a new multiplayer room */ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalProps) { const { mutateAsync: createRoom, isPending } = useCreateRoom() + const { getRoomShareUrl } = useRoomData() const availableGames = getAvailableGames() const [error, setError] = useState('') const [gameName, setGameName] = useState('__choose_later__') // Special value = user will choose later @@ -34,12 +38,16 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP 'open' | 'password' | 'approval-only' | 'restricted' >('open') const [password, setPassword] = useState('') + const [modalState, setModalState] = useState('creating') + const [createdRoomCode, setCreatedRoomCode] = useState(null) const handleClose = () => { setError('') setGameName('__choose_later__') setAccessMode('open') setPassword('') + setModalState('creating') + setCreatedRoomCode(null) onClose() } @@ -65,7 +73,7 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP const selectedGame = gameName === '__choose_later__' ? availableGames[0]?.manifest.name || 'matching' : gameName - await createRoom({ + const newRoom = await createRoom({ name, gameName: selectedGame, creatorName: 'Player', @@ -74,14 +82,21 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP password: accessMode === 'password' ? password : undefined, }) - // Success! Close modal - handleClose() - onSuccess?.() + // Success! Transition to success view + setCreatedRoomCode(newRoom.code) + setModalState('created') } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create room') } } + const handleStartPlaying = () => { + handleClose() + onSuccess?.() + } + + const shareUrl = createdRoomCode ? getRoomShareUrl(createdRoomCode) : '' + return (
-

- Create New Room -

-

- You'll leave the current room and create a new one -

- -
- {/* Room Name */} -
-
- - {/* Game Selection */} -
- - - { - if (!isPending) { - e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)' - e.currentTarget.style.borderColor = 'rgba(34, 197, 94, 0.5)' - } - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)' - e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.5)' - }} - > - - - - - + You'll leave the current room and create a new one +

- - + {/* Room Name */} +
+ + { + e.currentTarget.style.borderColor = 'rgba(34, 197, 94, 0.6)' + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.5)' + }} + /> +
+ + {/* Game Selection */} +
+ + + { + if (!isPending) { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)' + e.currentTarget.style.borderColor = 'rgba(34, 197, 94, 0.5)' + } + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)' + e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.5)' }} > - + + + + + + + { - 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' - } + background: 'linear-gradient(180deg, #1f2937 0%, #111827 100%)', + borderRadius: '12px', + border: '2px solid rgba(34, 197, 94, 0.3)', + padding: '6px', + boxShadow: '0 10px 40px rgba(0, 0, 0, 0.5)', + zIndex: 1000, + minWidth: '300px', + maxWidth: 'min(400px, 90vw)', + maxHeight: 'min(300px, 50vh)', + overflow: 'hidden', + position: 'relative', }} > - -
- -
-
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 ( + + { + e.currentTarget.style.opacity = '0.7' + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = '1' + }} + > + ▲ + + + { - if (gameName !== gameId) { - e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)' + if (gameName !== '__choose_later__') { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)' } }} onMouseLeave={(e) => { - if (gameName !== gameId) { + if (gameName !== '__choose_later__') { e.currentTarget.style.background = 'transparent' } }} > -
- {game.manifest.icon} -
-
- {game.manifest.displayName} +
+ +
+
+ Choose later
- {game.manifest.description} -
-
- - {game.manifest.maxPlayers === 1 - ? '👤 Solo' - : `👥 ${game.manifest.maxPlayers}p`} - - - {game.manifest.difficulty} - + 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 */} +
+ +
+ {[ + { value: 'open', emoji: '🌐', label: 'Open', desc: 'Anyone' }, + { value: 'password', emoji: '🔑', label: 'Password', desc: 'With key' }, + { value: 'approval-only', emoji: '✋', label: 'Approval', desc: 'Request' }, + { value: 'restricted', emoji: '🚫', label: 'Restricted', desc: 'Invite only' }, + ].map((mode) => ( +
+ {mode.emoji} +
+
{mode.label}
+
{mode.desc}
+
+ + ))} +
+
- {/* Access Mode Selection */} -
- -
- {[ - { value: 'open', emoji: '🌐', label: 'Open', desc: 'Anyone' }, - { value: 'password', emoji: '🔑', label: 'Password', desc: 'With key' }, - { value: 'approval-only', emoji: '✋', label: 'Approval', desc: 'Request' }, - { value: 'restricted', emoji: '🚫', label: 'Restricted', desc: 'Invite only' }, - ].map((mode) => ( - - ))} -
-
- - {accessMode === 'password' && ( -
-
+ + + ) : ( + <> + {/* Success View */} +
+
- Room Password - - setPassword(e.target.value)} - placeholder="Enter a password" - disabled={isPending} + ✓ +
+
{ - e.currentTarget.style.borderColor = 'rgba(34, 197, 94, 0.6)' - }} - onBlur={(e) => { - e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.5)' - }} - /> + > +

+ Room Created! +

+

+ Ready to play +

+
+ + {/* Share buttons */} + {createdRoomCode && ( +
+
+ Invite friends (optional) +
+ +
+ )} + + {/* Action buttons */} +
+ + +
- )} - - {error && ( -

- {error} -

- )} - -
- - -
- + + )}
) diff --git a/apps/web/src/components/nav/RoomShareButtons.tsx b/apps/web/src/components/nav/RoomShareButtons.tsx index c5e73da0..65ccebbc 100644 --- a/apps/web/src/components/nav/RoomShareButtons.tsx +++ b/apps/web/src/components/nav/RoomShareButtons.tsx @@ -19,31 +19,37 @@ export interface RoomShareButtonsProps { */ export function RoomShareButtons({ joinCode, shareUrl }: RoomShareButtonsProps) { return ( - <> - - 📋 - {joinCode} - - } - /> +
+ {/* Left side: stacked buttons */} +
+ + 📋 + {joinCode} + + } + style={{ marginBottom: 0 }} + /> - - 🔗 - Share Link - - } - copiedLabel="Link Copied!" - /> + + 🔗 + Share Link + + } + copiedLabel="Link Copied!" + style={{ marginBottom: 0 }} + /> +
+ {/* Right side: QR code button */} - +
) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72317c75..b66ebe1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: python-bridge: specifier: ^1.1.0 version: 1.1.0 + qrcode.react: + specifier: ^4.2.0 + version: 4.2.0(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -7726,6 +7729,11 @@ packages: python-bridge@1.1.0: resolution: {integrity: sha512-qjQ0QB8p9cn/XDeILQH0aP307hV58lrmv0Opjyub68Um7FHdF+ZXlTqyxNkKaXOFk2QSkScoPWwn7U9GGnrkeQ==} + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -17587,6 +17595,10 @@ snapshots: dependencies: bluebird: 3.7.2 + qrcode.react@4.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + qs@6.13.0: dependencies: side-channel: 1.1.0