Spaces:
Runtime error
Runtime error
Pedro de Carvalho commited on
Commit ·
0cc77af
1
Parent(s): 214b32d
Add UI components
Browse files
client/src/components/GameCanvas.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from 'react'
|
| 2 |
+
import { GameEngine } from '../game/GameEngine.ts'
|
| 3 |
+
|
| 4 |
+
interface Props {
|
| 5 |
+
onEngineReady?: (engine: GameEngine) => void
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function GameCanvas({ onEngineReady }: Props) {
|
| 9 |
+
const containerRef = useRef<HTMLDivElement>(null)
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
const container = containerRef.current
|
| 13 |
+
if (!container) return
|
| 14 |
+
|
| 15 |
+
const engine = new GameEngine()
|
| 16 |
+
engine.init(container).then(() => {
|
| 17 |
+
if (engine.destroyed) return
|
| 18 |
+
onEngineReady?.(engine)
|
| 19 |
+
})
|
| 20 |
+
|
| 21 |
+
return () => engine.destroy()
|
| 22 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 23 |
+
}, [])
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div
|
| 27 |
+
ref={containerRef}
|
| 28 |
+
style={{ width: '100%', height: '100%' }}
|
| 29 |
+
/>
|
| 30 |
+
)
|
| 31 |
+
}
|
client/src/components/SymbolPicker.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { theme } from '../theme.ts'
|
| 3 |
+
|
| 4 |
+
interface Props {
|
| 5 |
+
current: string
|
| 6 |
+
onSelect: (symbol: string) => void
|
| 7 |
+
onClose: () => void
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
type Category = 'classic' | 'hearts' | 'emojis' | 'letters'
|
| 11 |
+
|
| 12 |
+
const CATEGORIES: { id: Category; label: string }[] = [
|
| 13 |
+
{ id: 'classic', label: 'Classic' },
|
| 14 |
+
{ id: 'hearts', label: 'Hearts' },
|
| 15 |
+
{ id: 'emojis', label: 'Emojis' },
|
| 16 |
+
{ id: 'letters', label: 'Letters' },
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
const SYMBOLS: Record<Category, string[]> = {
|
| 20 |
+
classic: ['X', 'O', '●', '○', '★', '☆', '♠', '♥', '♦', '♣', '⚫', '⚪', '🔵', '🔴'],
|
| 21 |
+
hearts: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💝', '💖', '💗'],
|
| 22 |
+
emojis: [
|
| 23 |
+
'😀', '😍', '🤩', '😎', '😂', '🥳', '🤯', '😈', '👻', '👽',
|
| 24 |
+
'🚀', '⭐', '🌟', '🎯', '🔥', '💎', '🏆', '👑', '🎮', '🎲',
|
| 25 |
+
'🐱', '🐶', '🦊', '🐼', '🦄', '🐸', '🐨', '🐲', '🐬', '🦋',
|
| 26 |
+
'🌈', '⚡', '🍀', '🌺', '🌸', '🌻', '🌍', '🌙', '☀️', '❄️',
|
| 27 |
+
'🍕', '🍔', '🌮', '🍩', '🍪', '🍉', '🍇', '🍓',
|
| 28 |
+
'⚽', '🏀', '🎸', '🎹', '🎨', '🎭', '📷', '💡',
|
| 29 |
+
'♠️', '♥️', '♦️', '♣️', '♟️', '🎰', '🔮', '🗝️',
|
| 30 |
+
'💪', '🧠', '👀', '🫶', '🤝', '🎀', '🧩', '🪄',
|
| 31 |
+
],
|
| 32 |
+
letters: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export default function SymbolPicker({ current, onSelect, onClose }: Props) {
|
| 36 |
+
const [category, setCategory] = useState<Category>('classic')
|
| 37 |
+
const symbols = SYMBOLS[category]
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div
|
| 41 |
+
style={{
|
| 42 |
+
position: 'fixed', inset: 0, zIndex: 100,
|
| 43 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 44 |
+
background: 'rgba(0,0,0,0.5)',
|
| 45 |
+
animation: 'fadeIn 0.15s ease',
|
| 46 |
+
}}
|
| 47 |
+
onClick={onClose}
|
| 48 |
+
>
|
| 49 |
+
<div
|
| 50 |
+
style={{
|
| 51 |
+
background: theme.bgStr,
|
| 52 |
+
border: `1px solid ${theme.border}`,
|
| 53 |
+
borderRadius: 12,
|
| 54 |
+
padding: '1.25rem',
|
| 55 |
+
width: 'min(340px, 85vw)',
|
| 56 |
+
}}
|
| 57 |
+
onClick={(e) => e.stopPropagation()}
|
| 58 |
+
>
|
| 59 |
+
<span style={{
|
| 60 |
+
color: theme.text, fontWeight: 600, fontSize: '1rem',
|
| 61 |
+
display: 'block', marginBottom: '0.75rem',
|
| 62 |
+
}}>
|
| 63 |
+
Choose Your Symbol
|
| 64 |
+
</span>
|
| 65 |
+
|
| 66 |
+
<div style={{
|
| 67 |
+
display: 'flex', gap: '0.4rem', marginBottom: '0.75rem',
|
| 68 |
+
}}>
|
| 69 |
+
{CATEGORIES.map((cat) => (
|
| 70 |
+
<button
|
| 71 |
+
key={cat.id}
|
| 72 |
+
onClick={() => setCategory(cat.id)}
|
| 73 |
+
style={{
|
| 74 |
+
flex: 1,
|
| 75 |
+
padding: '0.35rem 0.25rem',
|
| 76 |
+
fontSize: '0.75rem',
|
| 77 |
+
fontWeight: 600,
|
| 78 |
+
cursor: 'pointer',
|
| 79 |
+
background: category === cat.id ? theme.player1Str : theme.bgPanel,
|
| 80 |
+
color: category === cat.id ? '#fff' : theme.textMuted,
|
| 81 |
+
border: category === cat.id ? `1px solid ${theme.player1Str}` : `1px solid ${theme.border}`,
|
| 82 |
+
borderRadius: 6,
|
| 83 |
+
transition: 'all 0.15s ease',
|
| 84 |
+
whiteSpace: 'nowrap',
|
| 85 |
+
}}
|
| 86 |
+
>
|
| 87 |
+
{cat.label}
|
| 88 |
+
</button>
|
| 89 |
+
))}
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div style={{
|
| 93 |
+
display: 'flex', flexWrap: 'wrap', gap: '0.6rem',
|
| 94 |
+
justifyContent: 'center',
|
| 95 |
+
maxHeight: 220,
|
| 96 |
+
overflowY: 'auto',
|
| 97 |
+
}}>
|
| 98 |
+
{symbols.map((s) => (
|
| 99 |
+
<button
|
| 100 |
+
key={s}
|
| 101 |
+
onClick={() => { onSelect(s); onClose() }}
|
| 102 |
+
style={{
|
| 103 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 104 |
+
width: 48, height: 48,
|
| 105 |
+
background: s === current ? theme.player1Str : theme.bgPanel,
|
| 106 |
+
border: s === current ? `2px solid ${theme.player1Str}` : `1px solid ${theme.border}`,
|
| 107 |
+
borderRadius: 8,
|
| 108 |
+
cursor: 'pointer',
|
| 109 |
+
fontSize: category === 'letters' ? '1.1rem' : '1.3rem',
|
| 110 |
+
color: s === current ? '#fff' : theme.text,
|
| 111 |
+
transition: 'all 0.15s ease',
|
| 112 |
+
flexShrink: 0,
|
| 113 |
+
}}
|
| 114 |
+
title={s}
|
| 115 |
+
>
|
| 116 |
+
{s}
|
| 117 |
+
</button>
|
| 118 |
+
))}
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
)
|
| 123 |
+
}
|
client/src/components/ThemeSelector.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { THEMES, theme } from '../theme.ts'
|
| 2 |
+
import type { ThemeMode } from '../theme.ts'
|
| 3 |
+
|
| 4 |
+
interface Props {
|
| 5 |
+
current: ThemeMode
|
| 6 |
+
onSelect: (mode: ThemeMode) => void
|
| 7 |
+
onClose: () => void
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const modes = Object.keys(THEMES) as ThemeMode[]
|
| 11 |
+
|
| 12 |
+
export default function ThemeSelector({ current, onSelect, onClose }: Props) {
|
| 13 |
+
return (
|
| 14 |
+
<div
|
| 15 |
+
style={{
|
| 16 |
+
position: 'fixed', inset: 0, zIndex: 100,
|
| 17 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 18 |
+
background: 'rgba(0,0,0,0.5)',
|
| 19 |
+
animation: 'fadeIn 0.15s ease',
|
| 20 |
+
}}
|
| 21 |
+
onClick={onClose}
|
| 22 |
+
>
|
| 23 |
+
<div
|
| 24 |
+
style={{
|
| 25 |
+
background: theme.bgStr,
|
| 26 |
+
border: `1px solid ${theme.border}`,
|
| 27 |
+
borderRadius: 12,
|
| 28 |
+
padding: '1.25rem',
|
| 29 |
+
width: 'min(360px, 85vw)',
|
| 30 |
+
}}
|
| 31 |
+
onClick={(e) => e.stopPropagation()}
|
| 32 |
+
>
|
| 33 |
+
<span style={{
|
| 34 |
+
color: theme.text, fontWeight: 600, fontSize: '1rem',
|
| 35 |
+
display: 'block', marginBottom: '1rem',
|
| 36 |
+
}}>
|
| 37 |
+
Choose Theme
|
| 38 |
+
</span>
|
| 39 |
+
<div style={{
|
| 40 |
+
display: 'flex', flexWrap: 'wrap', gap: '0.6rem',
|
| 41 |
+
justifyContent: 'center',
|
| 42 |
+
}}>
|
| 43 |
+
{modes.map((mode) => {
|
| 44 |
+
const t = THEMES[mode]
|
| 45 |
+
const isActive = mode === current
|
| 46 |
+
return (
|
| 47 |
+
<button
|
| 48 |
+
key={mode}
|
| 49 |
+
onClick={() => { onSelect(mode); onClose() }}
|
| 50 |
+
style={{
|
| 51 |
+
display: 'flex', flexDirection: 'column',
|
| 52 |
+
alignItems: 'center', justifyContent: 'center',
|
| 53 |
+
gap: '0.3rem',
|
| 54 |
+
width: 'min(140px, calc(50% - 0.6rem))',
|
| 55 |
+
minHeight: 64,
|
| 56 |
+
padding: '0.6rem 0.4rem',
|
| 57 |
+
background: t.colors.bgPanel,
|
| 58 |
+
border: isActive ? `2px solid ${t.colors.player1Str}` : `1px solid ${theme.border}`,
|
| 59 |
+
borderRadius: 8,
|
| 60 |
+
cursor: 'pointer',
|
| 61 |
+
color: t.colors.text,
|
| 62 |
+
fontSize: '0.8rem',
|
| 63 |
+
fontWeight: isActive ? 600 : 400,
|
| 64 |
+
transition: 'border 0.15s ease',
|
| 65 |
+
}}
|
| 66 |
+
>
|
| 67 |
+
<span style={{ fontSize: '1.4rem' }}>{t.emoji}</span>
|
| 68 |
+
<span>{t.label}</span>
|
| 69 |
+
</button>
|
| 70 |
+
)
|
| 71 |
+
})}
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
)
|
| 76 |
+
}
|