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
+ }