diff --git a/experimental/2nub/.claude/settings.local.json b/experimental/2nub/.claude/settings.local.json index 466fe3b..e1b4c77 100644 --- a/experimental/2nub/.claude/settings.local.json +++ b/experimental/2nub/.claude/settings.local.json @@ -15,7 +15,10 @@ "Bash(npm run:*)", "Bash(npx tsc:*)", "Bash(npm install)", - "WebFetch(domain:socket.io)" + "WebFetch(domain:socket.io)", + "Bash(timeout 10s npm run dev:client)", + "Bash(rm:*)", + "Bash(timeout 8s npm run dev:client)" ], "deny": [], "ask": [] diff --git a/experimental/2nub/package-lock.json b/experimental/2nub/package-lock.json index 34f4e7e..d58ba5a 100644 --- a/experimental/2nub/package-lock.json +++ b/experimental/2nub/package-lock.json @@ -17,6 +17,7 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { + "@parcel/transformer-sass": "^2.16.1", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jest": "^29.5.0", @@ -2480,6 +2481,26 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/transformer-sass": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/transformer-sass/-/transformer-sass-2.16.1.tgz", + "integrity": "sha512-ypLLM+d8YTZveUEoXiCtk+J9Q1sHtjhq9kFANTbAgEvJB4dp2hZNHEQ63Y49Mh9m4IdnBQ1L5oAvUtYzVujFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.1", + "@parcel/source-map": "^2.1.1", + "sass": "^1.88.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/transformer-svg": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.16.1.tgz", @@ -3915,6 +3936,22 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -5218,6 +5255,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -7272,6 +7316,20 @@ "node": ">=0.10.0" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -7396,6 +7454,27 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass": { + "version": "1.94.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", + "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -7788,6 +7867,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", diff --git a/experimental/2nub/package.json b/experimental/2nub/package.json index 6cc1336..df2e9f4 100644 --- a/experimental/2nub/package.json +++ b/experimental/2nub/package.json @@ -22,6 +22,7 @@ "socket.io-client": "^4.7.5" }, "devDependencies": { + "@parcel/transformer-sass": "^2.16.1", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/jest": "^29.5.0", diff --git a/experimental/2nub/src/client/App.tsx b/experimental/2nub/src/client/App.tsx index c3c02f1..93d7986 100644 --- a/experimental/2nub/src/client/App.tsx +++ b/experimental/2nub/src/client/App.tsx @@ -7,6 +7,7 @@ import { SocketEventProvider } from './contexts/SocketEventContext'; import { GameState, GameStateClient } from '../types'; import { buildApiUrl } from './config/api'; import { sessionStorage } from './utils/sessionStorage'; +import './styles/main.scss'; const App: React.FC = () => { const [games, setGames] = useState([]); @@ -224,59 +225,21 @@ const App: React.FC = () => { return ( -
-
-

2nub Game Boilerplate

-
-
-
+
+
+

2nub Game Boilerplate

+
+
+
{isConnected ? 'Connected' : 'Disconnected'}
{!isConnected && ( - )} {view === 'game' && ( - )} @@ -284,7 +247,7 @@ const App: React.FC = () => {
{view === 'list' ? ( -
+
diff --git a/experimental/2nub/src/client/components/CreateGame.tsx b/experimental/2nub/src/client/components/CreateGame.tsx index 65d6d24..539eba2 100644 --- a/experimental/2nub/src/client/components/CreateGame.tsx +++ b/experimental/2nub/src/client/components/CreateGame.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import '../styles/main.scss'; interface CreateGameProps { onCreateGame: (name: string) => void; @@ -16,16 +17,11 @@ export const CreateGame: React.FC = ({ onCreateGame }) => { }; return ( -
-

Create New Game

-
+
+

Create New Game

+
-
- diff --git a/experimental/2nub/src/client/components/GameBoard.tsx b/experimental/2nub/src/client/components/GameBoard.tsx index 7901df9..5a257ed 100644 --- a/experimental/2nub/src/client/components/GameBoard.tsx +++ b/experimental/2nub/src/client/components/GameBoard.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { GameStateClient } from '../../types'; import { Onub } from './Onub'; +import '../styles/main.scss'; interface GameBoardProps { game: GameStateClient | null; @@ -27,39 +28,11 @@ export const GameBoard: React.FC = ({ game, gameId, currentPlaye // If we have a gameId but no game data, show loading state if (gameId) { return ( -
+

Loading game...

Connecting to game and loading data...

-
- -
@@ -68,23 +41,10 @@ export const GameBoard: React.FC = ({ game, gameId, currentPlaye // No gameId means the game doesn't exist return ( -
+

Game not found

The game you were in no longer exists.

-
@@ -97,32 +57,14 @@ export const GameBoard: React.FC = ({ game, gameId, currentPlaye const isCurrentPlayerDisconnected = currentPlayer && !currentPlayer.connected; return ( -
-
+
+
-

{game.name}

-
+

{game.name}

+
Game ID: {gameId} Players: {game.players.length} - + {game.state.state === 'lobby' ? 'Waiting' : game.state.state}
@@ -130,24 +72,12 @@ export const GameBoard: React.FC = ({ game, gameId, currentPlaye
{isCurrentPlayerDisconnected && ( -
-
-
-
+
+
+
+
You are currently disconnected -

+

Trying to reconnect... You can refresh the page or use the rejoin feature if needed.

@@ -156,59 +86,36 @@ export const GameBoard: React.FC = ({ game, gameId, currentPlaye )} {currentPlayer && ( -
-

You are:

-

+

+

You are:

+

{currentPlayer.name} (Seat {game.players.indexOf(currentPlayer) + 1})

)} -
-

Players in Game:

+
+

Players in Game:

{playerList.length === 0 ? ( -

No players in the game yet.

+

No players in the game yet.

) : ( -
+
{playerList.map((player) => ( -
-
-
+
+
+
{player.name} {player.id === currentPlayerId && '(You)'}
-
+
Seat {game.players.indexOf(player) + 1}
-
+
{player.connected && player.id !== currentPlayerId && (
diff --git a/experimental/2nub/src/client/components/GameList.tsx b/experimental/2nub/src/client/components/GameList.tsx index 770bf58..5f858d4 100644 --- a/experimental/2nub/src/client/components/GameList.tsx +++ b/experimental/2nub/src/client/components/GameList.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { GameState } from '../../types'; +import '../styles/main.scss'; interface GameListProps { games: GameState[]; @@ -67,83 +68,55 @@ export const GameList: React.FC = ({ games, onJoinGame, onRejoinG } }; + const getStatusClass = (state: string) => { + switch (state) { + case 'lobby': return 'waiting'; + case 'night': + case 'day': + case 'voting': return 'night'; + case 'finished': return 'finished'; + default: return 'finished'; + } + }; + return ( -
-
-

Available Games

-
{games.length === 0 ? ( -

+

No games available. Create one to get started!

) : ( -
+
{games.map((game) => ( -
-
-
-

{game.name}

-
- +
+
+
+

{game.name}

+
+ ID: {game.id} - + Players: {game.players.length} - + {getStatusDisplayName(game.state.state)}
{game.players.length > 0 && ( -
- Players: -
+
+ Players: +
{game.players.map((player) => ( - + {player.name} (Seat {game.players.indexOf(player) + 1}) ))} @@ -152,21 +125,21 @@ export const GameList: React.FC = ({ games, onJoinGame, onRejoinG )}
-
+
{joiningGameId === game.id ? ( -
+
{(() => { const disconnectedPlayers = game.players.filter(p => !p.connected); return ( -
handleJoinSubmit(e, game.id)} style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> + handleJoinSubmit(e, game.id)} className="form"> {disconnectedPlayers.length > 0 && ( -
-
+
+
Join as disconnected player:
-
+
{disconnectedPlayers.map((player) => ( -
)} -
0 ? '1px solid #ddd' : 'none', paddingTop: disconnectedPlayers.length > 0 ? '8px' : '0' }}> -