fix(qr-button): improve layout and z-index
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
349290ac6a
commit
646a4228d0
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '16px' }}>📱</span>
|
||||
<span>QR Code</span>
|
||||
<QRCodeSVG value={url} size={40} level="L" />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
|
|
@ -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',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<string>('__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<ModalState>('creating')
|
||||
const [createdRoomCode, setCreatedRoomCode] = useState<string | null>(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 (
|
||||
<Modal isOpen={isOpen} onClose={handleClose}>
|
||||
<div
|
||||
|
|
@ -89,8 +104,11 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP
|
|||
border: '2px solid rgba(34, 197, 94, 0.3)',
|
||||
borderRadius: '16px',
|
||||
padding: '24px',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{modalState === 'creating' ? (
|
||||
<>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
|
|
@ -300,7 +318,9 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP
|
|||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<span style={{ fontSize: '20px' }}>✨</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: '500', fontSize: '14px' }}>Choose later</div>
|
||||
<div style={{ fontWeight: '500', fontSize: '14px' }}>
|
||||
Choose later
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
|
|
@ -658,6 +678,133 @@ export function CreateRoomModal({ isOpen, onClose, onSuccess }: CreateRoomModalP
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Success View */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '48px',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
color: 'rgba(134, 239, 172, 1)',
|
||||
}}
|
||||
>
|
||||
Room Created!
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
color: 'rgba(209, 213, 219, 0.8)',
|
||||
}}
|
||||
>
|
||||
Ready to play
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Share buttons */}
|
||||
{createdRoomCode && (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: 'rgba(156, 163, 175, 1)',
|
||||
textAlign: 'center',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
Invite friends (optional)
|
||||
</div>
|
||||
<RoomShareButtons joinCode={createdRoomCode} shareUrl={shareUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div style={{ display: 'flex', gap: '12px', width: '100%', marginTop: '8px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
background: 'rgba(75, 85, 99, 0.3)',
|
||||
color: 'rgba(209, 213, 219, 1)',
|
||||
border: '2px solid rgba(75, 85, 99, 0.5)',
|
||||
borderRadius: '10px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(75, 85, 99, 0.4)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(75, 85, 99, 0.3)'
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStartPlaying}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(34, 197, 94, 0.8), rgba(22, 163, 74, 0.8))',
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
border: '2px solid rgba(34, 197, 94, 0.6)',
|
||||
borderRadius: '10px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background =
|
||||
'linear-gradient(135deg, rgba(34, 197, 94, 0.9), rgba(22, 163, 74, 0.9))'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background =
|
||||
'linear-gradient(135deg, rgba(34, 197, 94, 0.8), rgba(22, 163, 74, 0.8))'
|
||||
}}
|
||||
>
|
||||
Start Playing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ export interface RoomShareButtonsProps {
|
|||
*/
|
||||
export function RoomShareButtons({ joinCode, shareUrl }: RoomShareButtonsProps) {
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: '6px', marginBottom: '6px' }}>
|
||||
{/* Left side: stacked buttons */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: 1 }}>
|
||||
<CopyButton
|
||||
text={joinCode}
|
||||
variant="code"
|
||||
|
|
@ -29,6 +31,7 @@ export function RoomShareButtons({ joinCode, shareUrl }: RoomShareButtonsProps)
|
|||
<span>{joinCode}</span>
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 0 }}
|
||||
/>
|
||||
|
||||
<CopyButton
|
||||
|
|
@ -41,9 +44,12 @@ export function RoomShareButtons({ joinCode, shareUrl }: RoomShareButtonsProps)
|
|||
</>
|
||||
}
|
||||
copiedLabel="Link Copied!"
|
||||
style={{ marginBottom: 0 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side: QR code button */}
|
||||
<QRCodeButton url={shareUrl} />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue