Compare commits
2 Commits
v2.0.0-rc3
...
v2.0.0
103
README.md
Normal file
103
README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Connect4 Moderator Observer UI
|
||||
|
||||
A website interface for connecting to the [Connect4 WebSocket server](https://github.com/joshuafhiggins/connect4-moderator-server) as an observer or player, watching live matches, and managing tournaments.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux/macOS terminal or Windows PowerShell
|
||||
- Git
|
||||
- Docker (optional, for containerized runs)
|
||||
|
||||
## Run Locally (Node.js + npm)
|
||||
|
||||
### 1) Install Node.js (includes npm)
|
||||
|
||||
#### Option A: Install with `nvm` (recommended, including Windows support)
|
||||
|
||||
On Linux/macOS:
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||
source ~/.zshrc
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
On Windows (PowerShell), use `nvm-windows`:
|
||||
|
||||
```powershell
|
||||
winget install CoreyButler.NVMforWindows
|
||||
nvm install lts
|
||||
nvm use lts
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
### 2) Install dependencies
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
### 3) Start the UI in development mode
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Run with Docker
|
||||
|
||||
This repository includes:
|
||||
|
||||
- `Dockerfile` (multi-stage production build)
|
||||
- `docker-compose.yml`
|
||||
- `docker_build.sh`
|
||||
|
||||
### Build image directly from `Dockerfile`
|
||||
|
||||
```bash
|
||||
docker build . -t joshuafhiggins/connect4-ui
|
||||
```
|
||||
|
||||
or use the provided script:
|
||||
|
||||
```bash
|
||||
./docker_build.sh
|
||||
```
|
||||
|
||||
### Run container directly
|
||||
|
||||
```bash
|
||||
docker run --rm -p 3000:3000 --name connect4-ui joshuafhiggins/connect4-ui
|
||||
```
|
||||
|
||||
### Run with Docker Compose
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Run detached:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Stop:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Useful npm Scripts
|
||||
|
||||
- `npm run dev` - start Next.js dev server
|
||||
- `npm run build` - build for production
|
||||
- `npm run start` - run production build
|
||||
- `npm run lint` - lint the project
|
||||
@@ -23,6 +23,8 @@ export default function PlayPage() {
|
||||
role,
|
||||
username,
|
||||
status,
|
||||
isInMatch,
|
||||
shouldGoFirst,
|
||||
send,
|
||||
subscribe,
|
||||
reconnectAttempts,
|
||||
@@ -86,13 +88,25 @@ export default function PlayPage() {
|
||||
}
|
||||
|
||||
if (status === "connected" && gamePhase === "idle") {
|
||||
setGamePhase("connected");
|
||||
// Mid-match reconnect can remount with phase idle while still in a match; avoid
|
||||
// the pre-queue "connected" / Ready Up state until we know we are not in-game.
|
||||
setGamePhase(isInMatch ? "playing" : "connected");
|
||||
if (isInMatch) {
|
||||
const color: 1 | 2 = shouldGoFirst ? 1 : 2;
|
||||
setMyColor(color);
|
||||
myColorRef.current = color;
|
||||
|
||||
setIsMyTurn(shouldGoFirst);
|
||||
isMyTurnRef.current = shouldGoFirst;
|
||||
}
|
||||
|
||||
}
|
||||
}, [
|
||||
role,
|
||||
status,
|
||||
router,
|
||||
gamePhase,
|
||||
isInMatch,
|
||||
shouldRedirectToConnect,
|
||||
clearRedirectFlag,
|
||||
]);
|
||||
@@ -102,7 +116,11 @@ export default function PlayPage() {
|
||||
switch (msg.type) {
|
||||
case "CONNECT_ACK":
|
||||
case "RECONNECT_ACK":
|
||||
setGamePhase((prev) => (prev === "idle" ? "connected" : prev));
|
||||
setGamePhase((prev) => {
|
||||
if (prev !== "idle") return prev;
|
||||
if (isInMatch) return "playing";
|
||||
return "connected";
|
||||
});
|
||||
break;
|
||||
|
||||
case "ERROR":
|
||||
@@ -193,7 +211,7 @@ export default function PlayPage() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [resetGame, send, subscribe]);
|
||||
}, [resetGame, send, subscribe, isInMatch, username]);
|
||||
|
||||
const handleColumnClick = useCallback(
|
||||
(col: number) => {
|
||||
|
||||
@@ -152,6 +152,7 @@ export default function AdminSettingsPanel() {
|
||||
break;
|
||||
case "RESERVATION_LIST":
|
||||
setReservations(message.reservations);
|
||||
setActionFeedback("Loaded reservations.");
|
||||
break;
|
||||
case "RESERVATION_ADD":
|
||||
setReservations((prev) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ interface ConnectionContextValue {
|
||||
username: string;
|
||||
status: ConnectionStatus;
|
||||
isInMatch: boolean;
|
||||
shouldGoFirst: boolean;
|
||||
isAdmin: boolean;
|
||||
reconnectAttempts: number;
|
||||
shouldRedirectToConnect: boolean;
|
||||
@@ -70,6 +71,7 @@ export function ConnectionProvider({
|
||||
const [username, setUsername] = useState("");
|
||||
const [status, setStatus] = useState<ConnectionStatus>("idle");
|
||||
const [isInMatch, setIsInMatch] = useState(false);
|
||||
const [shouldGoFirst, setShouldGoFirst] = useState(false);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [reconnectAttempts, setReconnectAttempts] = useState(0);
|
||||
const [shouldRedirectToConnect, setShouldRedirectToConnect] = useState(false);
|
||||
@@ -81,6 +83,7 @@ export function ConnectionProvider({
|
||||
const reconnectDeadlineRef = useRef<number | null>(null);
|
||||
const reconnectActiveRef = useRef(false);
|
||||
const isInMatchRef = useRef(false);
|
||||
const shouldGoFirstRef = useRef(false);
|
||||
const sessionRef = useRef<SessionState | null>(null);
|
||||
|
||||
const clearReconnectTimer = useCallback(() => {
|
||||
@@ -202,6 +205,8 @@ export function ConnectionProvider({
|
||||
if (parsed.type === "GAME_START") {
|
||||
isInMatchRef.current = true;
|
||||
setIsInMatch(true);
|
||||
shouldGoFirstRef.current = parsed.goesFirst;
|
||||
setShouldGoFirst(parsed.goesFirst);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -377,6 +382,7 @@ export function ConnectionProvider({
|
||||
username,
|
||||
status,
|
||||
isInMatch,
|
||||
shouldGoFirst,
|
||||
isAdmin,
|
||||
reconnectAttempts,
|
||||
shouldRedirectToConnect,
|
||||
@@ -394,6 +400,7 @@ export function ConnectionProvider({
|
||||
username,
|
||||
status,
|
||||
isInMatch,
|
||||
shouldGoFirst,
|
||||
isAdmin,
|
||||
reconnectAttempts,
|
||||
shouldRedirectToConnect,
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -10,7 +14,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -18,10 +22,20 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user