Compare commits
11 Commits
7ee59c434b
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c1f5c76e0 | |||
| 36d72fd6cd | |||
| bd7ec581e3 | |||
| ff75c8ce2f | |||
| 2de003e497 | |||
| e83ec814d9 | |||
| 0d7bb0343c | |||
| 8b090e4d96 | |||
| 3e8c7c4057 | |||
| 4da882f481 | |||
| 8df2d0550a |
@@ -21,9 +21,9 @@ yarn-error.log
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
.vscode/
|
||||
.history/*
|
||||
.angualar/
|
||||
.angular/
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
## 0.0.0 (2026-05-12)
|
||||
|
||||
### Features
|
||||
|
||||
* added bot, light and dark mode ([2de003e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2de003e497baee72f998d0d805ca1e58aababe48))
|
||||
* added web view 1v1 ([1828fa3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1828fa3275ddb8ce6bb90a9f6a970ec428ebce3a))
|
||||
* NCS-63 User account implementation ([#2](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/2)) ([ff75c8c](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ff75c8ce2fad54137f04a14c15bc1d4a38fa9bb8))
|
||||
* NCS-75 Frontend Deployment Dockerfile ([#4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/4)) ([bd7ec58](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd7ec581e38b5d8e775741bf16e19b4dc67b979e))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build issues ([36d72fd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/36d72fd6cda41be51d28f8ac307dcdbcd31afa91))
|
||||
* cleaner components seperation ([8b090e4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8b090e4d96c64c0adb253e3aefad7930108ccfb9))
|
||||
* gitignore ([4da882f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/4da882f481ba7a008aac868fb37de7cb2bafea5d))
|
||||
* GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02))
|
||||
* npm ([c11c1d4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c11c1d4dce9de4bd5b463e891eebf961b37feb04))
|
||||
* removed .vs ([2833ead](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2833ead7be3b47ee5c188d2d21b7326cb3cb3f26))
|
||||
* removed cache files ([7ee59c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/7ee59c434bf137a08fd560bbc02ceefbcfd90f04))
|
||||
* size of pieces and removed text view of the game state ([c60d00f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c60d00f9d25247504845654615065fbccd7fe448))
|
||||
* structure ([3e8c7c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/3e8c7c4057e55aeec7cee8c24f6751ff24912c93))
|
||||
@@ -0,0 +1,25 @@
|
||||
FROM --platform=$BUILDPLATFORM node:lts-alpine3.23 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM --platform=$TARGETPLATFORM nginx:stable-alpine AS production
|
||||
|
||||
RUN apk add --no-cache gettext
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=builder /app/dist/nowchess-frontend/browser /usr/share/nginx/html
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY public/env.template.js /usr/share/nginx/html/env.template.js
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
@@ -1,60 +1,41 @@
|
||||
# NowchessFrontend
|
||||
# NowChess Frontend
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.24.
|
||||
Angular 20 frontend for the NowChess board UI.
|
||||
|
||||
## Development server
|
||||
## Tech stack
|
||||
|
||||
To start a local development server, run:
|
||||
- Angular standalone components and route-based pages
|
||||
- HTTP and streaming integration for live game updates
|
||||
- Asset sprites loaded from `arabian-chess/`
|
||||
|
||||
## Project structure
|
||||
|
||||
- `src/app/pages` page-level containers (`welcome`, `game`)
|
||||
- `src/app/components` reusable UI pieces (`chess-board`, `chess-piece`)
|
||||
- `src/app/services` API and stream integration (`GameApiService`)
|
||||
- `src/app/models` shared API/domain types
|
||||
- `src/app/core/chess` chess domain utilities (FEN parsing and square lookup)
|
||||
- `src/environments` environment-specific API base URLs
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
Open `http://localhost:4200`.
|
||||
|
||||
## Code scaffolding
|
||||
Development environment defaults to:
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
- API: `http://localhost:8080`
|
||||
- WebSocket: `ws://localhost:8080`
|
||||
|
||||
`src/environments/environment.ts` is production-oriented (`production: true`) and `src/environments/environment.development.ts` is development-oriented (`production: false`).
|
||||
|
||||
## Build and test
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "npm"
|
||||
"packageManager": "npm",
|
||||
"analytics": false
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
@@ -28,8 +29,13 @@
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "ARABIAN CHESS",
|
||||
"output": "/arabian-chess"
|
||||
"input": "assets/arabian-chess",
|
||||
"output": "/assets/arabian-chess"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "assets/ChessAssets",
|
||||
"output": "/assets/ChessAssets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
@@ -41,18 +47,32 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
"maximumWarning": "1MB",
|
||||
"maximumError": "5MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
"maximumWarning": "1MB",
|
||||
"maximumError": "5MB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"staging": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.staging.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"development": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
],
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
@@ -62,10 +82,18 @@
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"options": {
|
||||
"proxyConfig": "proxy.conf.json",
|
||||
"host": "0.0.0.0",
|
||||
"port": 4200
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "nowchess-frontend:build:production"
|
||||
},
|
||||
"staging": {
|
||||
"buildTarget": "nowchess-frontend:build:staging"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "nowchess-frontend:build:development"
|
||||
}
|
||||
@@ -90,8 +118,13 @@
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "ARABIAN CHESS",
|
||||
"output": "/arabian-chess"
|
||||
"input": "assets/arabian-chess",
|
||||
"output": "/assets/arabian-chess"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "assets/ChessAssets",
|
||||
"output": "/assets/ChessAssets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 518 KiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 907 B |
|
Before Width: | Height: | Size: 919 B After Width: | Height: | Size: 919 B |
|
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 818 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 161 B After Width: | Height: | Size: 161 B |
|
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 188 B |
|
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 188 B |
|
Before Width: | Height: | Size: 237 B After Width: | Height: | Size: 237 B |
|
Before Width: | Height: | Size: 243 B After Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 232 B |
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 211 B |
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 300 B |
|
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
|
Before Width: | Height: | Size: 266 B After Width: | Height: | Size: 266 B |
|
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 297 B |
|
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 258 B |
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 263 B |
|
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 313 B |
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 280 B After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Replace placeholders in env.template.js with environment variables and write env.js
|
||||
TEMPLATE_PATH="/usr/share/nginx/html/env.template.js"
|
||||
TARGET_PATH="/usr/share/nginx/html/env.js"
|
||||
|
||||
if [ -f "$TEMPLATE_PATH" ]; then
|
||||
echo "Rendering runtime config from $TEMPLATE_PATH"
|
||||
echo "Using environment variables:"
|
||||
printenv
|
||||
echo "----"
|
||||
envsubst < "$TEMPLATE_PATH" > "$TARGET_PATH"
|
||||
else
|
||||
echo "No runtime template found at $TEMPLATE_PATH, skipping"
|
||||
fi
|
||||
|
||||
exec nginx -g 'daemon off;'
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: NowChess API
|
||||
title: NowChess Board API
|
||||
description: |
|
||||
REST API for the NowChess application. Designed to feel familiar to users
|
||||
of the [lichess API](https://lichess.org/api).
|
||||
@@ -186,11 +186,8 @@ paths:
|
||||
currently to move.
|
||||
|
||||
For promotion moves include the target piece as the fifth character:
|
||||
`e7e8q`, `a2a1r`, etc.
|
||||
|
||||
If the move results in a pawn reaching the back rank and no promotion
|
||||
character is supplied, the game enters `promotionPending` status and
|
||||
the move is not yet applied — resubmit with the promotion character.
|
||||
`e7e8q`, `a2a1r`, etc. Promotion moves without the fifth character
|
||||
are rejected with `400 INVALID_MOVE`.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
@@ -630,7 +627,6 @@ components:
|
||||
| `draw` | Draw agreed or claimed — game over |
|
||||
| `drawOffered` | Waiting for the opponent to accept or decline a draw offer |
|
||||
| `fiftyMoveAvailable` | Fifty-move rule threshold reached; active player may claim draw |
|
||||
| `promotionPending` | A pawn reached the back rank; awaiting promotion piece selection |
|
||||
| `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) |
|
||||
enum:
|
||||
- started
|
||||
@@ -641,7 +637,6 @@ components:
|
||||
- draw
|
||||
- drawOffered
|
||||
- fiftyMoveAvailable
|
||||
- promotionPending
|
||||
- insufficientMaterial
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /env.js {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/javascript;
|
||||
expires -1;
|
||||
add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@@ -457,7 +458,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz",
|
||||
"integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -474,7 +474,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz",
|
||||
"integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -488,7 +487,6 @@
|
||||
"integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.28.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@@ -521,7 +519,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz",
|
||||
"integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -565,7 +562,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz",
|
||||
"integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -632,7 +628,6 @@
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -1606,7 +1601,6 @@
|
||||
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^4.2.1",
|
||||
"@inquirer/confirm": "^5.1.14",
|
||||
@@ -2969,6 +2963,17 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
@@ -3526,7 +3531,6 @@
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
@@ -3820,6 +3824,25 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.8",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
@@ -3864,7 +3887,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -4880,7 +4902,6 @@
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -5322,7 +5343,6 @@
|
||||
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -5819,8 +5839,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz",
|
||||
"integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
@@ -5922,7 +5941,6 @@
|
||||
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"body-parser": "^1.19.0",
|
||||
@@ -6390,7 +6408,6 @@
|
||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "^4.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -7903,7 +7920,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -7939,7 +7955,6 @@
|
||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -8559,8 +8574,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tuf-js": {
|
||||
"version": "4.1.0",
|
||||
@@ -8598,7 +8612,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8728,7 +8741,6 @@
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -9526,7 +9538,6 @@
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -9545,8 +9556,7 @@
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build": "ng build --configuration production",
|
||||
"build:staging": "ng build --configuration staging",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
@@ -28,6 +29,7 @@
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@@ -45,4 +47,4 @@
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"/api/account": {
|
||||
"target": "http://localhost:8083",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/api/challenge": {
|
||||
"target": "http://localhost:8083",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
},
|
||||
"/api": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"ws": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
window.__RUNTIME_CONFIG__ = {
|
||||
API_URL: "${API_URL}",
|
||||
WEBSOCKET_URL: "${WEBSOCKET_URL}"
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { authInterceptor } from './services/auth.interceptor';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideHttpClient(),
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideRouter(routes)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<router-outlet />
|
||||
<app-toolbar />
|
||||
<router-outlet />
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { GameComponent } from './pages/game/game.component';
|
||||
import { WelcomeComponent } from './pages/welcome/welcome.component';
|
||||
import { ProfileComponent } from './pages/profile/profile.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: WelcomeComponent },
|
||||
{ path: 'profile', component: ProfileComponent },
|
||||
{ path: 'game/:gameId', component: GameComponent },
|
||||
{ path: '**', redirectTo: '' }
|
||||
];
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ToolbarComponent } from './components/toolbar/toolbar.component';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, ToolbarComponent],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.css'
|
||||
})
|
||||
export class App {
|
||||
export class App implements OnInit {
|
||||
constructor(private readonly themeService: ThemeService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.themeService.initTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/* Shared Button Template - All Button Styles Consolidated */
|
||||
|
||||
.app-btn {
|
||||
background: var(--btn-bg);
|
||||
color: var(--btn-fg);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 1rem;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--btn-glow);
|
||||
transition: transform 0.2s ease, filter 0.2s ease, background 1.6s ease, box-shadow 1.6s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-btn:hover:enabled {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.app-btn:active:enabled {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.app-btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.app-btn.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Dialog Button Layouts */
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dialog-actions .app-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Promotion Dialog Button Variant */
|
||||
.promotion-choice {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.promotion-choice .piece-symbol {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.promotion-choice .piece-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -7,10 +7,10 @@
|
||||
.board-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
border: 2px solid #5A2C28;
|
||||
border-radius: 10px 10px 0 0;
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-radius: var(--border-radius-md) var(--border-radius-md) 0 0;
|
||||
overflow: hidden;
|
||||
background: #5A2C28;
|
||||
background: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.square {
|
||||
@@ -27,12 +27,56 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.square[draggable='true'] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.square.drag-source {
|
||||
opacity: 0.65;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.square.drag-over {
|
||||
outline: 3px dashed var(--color-primary);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
.square.light {
|
||||
background-image: url('/arabian-chess/sprites/board/board_square_white.png');
|
||||
background-image: url('/assets/arabian-chess/sprites/board/board_square_white.png');
|
||||
}
|
||||
|
||||
.square.dark {
|
||||
background-image: url('/arabian-chess/sprites/board/board_square_black.png');
|
||||
background-image: url('/assets/arabian-chess/sprites/board/board_square_black.png');
|
||||
}
|
||||
|
||||
.board-grid--classic {
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
.board-grid--classic .square {
|
||||
background-image: none;
|
||||
transition: filter 160ms ease;
|
||||
}
|
||||
|
||||
.board-grid--classic .square.light {
|
||||
background-color: #f3c8a0;
|
||||
}
|
||||
|
||||
.board-grid--classic .square.dark {
|
||||
background-color: #ba6d4b;
|
||||
}
|
||||
|
||||
.board-grid--classic .square.drag-over {
|
||||
outline-color: #5a2c28;
|
||||
}
|
||||
|
||||
.board-grid--classic .square.selected {
|
||||
outline-color: #5a2c28;
|
||||
}
|
||||
|
||||
.board-grid--classic .square.highlighted::after {
|
||||
background: #b9dad1;
|
||||
border-color: #5a2c28;
|
||||
}
|
||||
|
||||
.square.highlighted::after {
|
||||
@@ -41,12 +85,13 @@
|
||||
width: 28%;
|
||||
height: 28%;
|
||||
border-radius: 50%;
|
||||
background: rgba(193, 158, 245, 0.75);
|
||||
border: 2px solid #5A2C28;
|
||||
background: var(--color-secondary-purple);
|
||||
opacity: 0.75;
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
}
|
||||
|
||||
.square.selected {
|
||||
outline: 3px solid #BA6D4B;
|
||||
outline: 3px solid var(--color-primary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
@@ -55,7 +100,7 @@
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: fill;
|
||||
border: 2px solid #5A2C28;
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 10px 10px;
|
||||
border-radius: 0 0 var(--border-radius-md) var(--border-radius-md);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="board-shell">
|
||||
<div class="board-grid">
|
||||
<div class="board-grid" [class.board-grid--classic]="boardTheme === 'classic'" [class.board-grid--arabian]="boardTheme === 'arabian'">
|
||||
@for (square of squares; track trackByCoordinate($index, square)) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -8,13 +8,25 @@
|
||||
[class.dark]="!square.isLight"
|
||||
[class.selected]="isSelected(square)"
|
||||
[class.highlighted]="isHighlighted(square)"
|
||||
[class.drag-source]="isDraggingSource(square)"
|
||||
[class.drag-over]="isDragOver(square)"
|
||||
[attr.data-square]="square.coordinate"
|
||||
(click)="onSquareClick(square)"
|
||||
(dragover)="onSquareDragOver($event, square)"
|
||||
(drop)="onSquareDrop($event, square)"
|
||||
>
|
||||
<app-chess-piece [pieceCode]="square.pieceCode" />
|
||||
<app-chess-piece
|
||||
[pieceCode]="square.pieceCode"
|
||||
[boardTheme]="boardTheme"
|
||||
[draggable]="!!square.pieceCode"
|
||||
(pieceDragStart)="onPieceDragStart($event, square)"
|
||||
(pieceDragEnd)="onSquareDragEnd()"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<img class="board-bottom" src="/arabian-chess/sprites/board/board_bottom.png" alt="Board frame" />
|
||||
@if (boardTheme === 'arabian') {
|
||||
<img class="board-bottom" src="/assets/arabian-chess/sprites/board/board_bottom.png" alt="Board frame" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@ interface BoardSquare {
|
||||
pieceCode: string | null;
|
||||
}
|
||||
|
||||
type BoardTheme = 'arabian' | 'classic';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chess-board',
|
||||
standalone: true,
|
||||
@@ -18,10 +20,14 @@ export class ChessBoardComponent implements OnChanges {
|
||||
@Input({ required: true }) fen = '';
|
||||
@Input() selectedSquare: string | null = null;
|
||||
@Input() highlightedSquares: string[] = [];
|
||||
@Input() boardTheme: BoardTheme = 'arabian';
|
||||
@Output() squareSelected = new EventEmitter<string>();
|
||||
|
||||
squares: BoardSquare[] = [];
|
||||
private highlightedSquareSet = new Set<string>();
|
||||
private draggingFromSquare: string | null = null;
|
||||
private dragOverSquare: string | null = null;
|
||||
private suppressNextClick = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['fen']) {
|
||||
@@ -38,9 +44,61 @@ export class ChessBoardComponent implements OnChanges {
|
||||
}
|
||||
|
||||
onSquareClick(square: BoardSquare): void {
|
||||
if (this.suppressNextClick) {
|
||||
this.suppressNextClick = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.squareSelected.emit(square.coordinate);
|
||||
}
|
||||
|
||||
onPieceDragStart(event: DragEvent, square: BoardSquare): void {
|
||||
if (!square.pieceCode) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.draggingFromSquare = square.coordinate;
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('text/plain', square.coordinate);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
this.squareSelected.emit(square.coordinate);
|
||||
}
|
||||
|
||||
onSquareDragOver(event: DragEvent, square: BoardSquare): void {
|
||||
if (!this.draggingFromSquare) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
this.dragOverSquare = square.coordinate === this.draggingFromSquare ? null : square.coordinate;
|
||||
}
|
||||
|
||||
onSquareDrop(event: DragEvent, square: BoardSquare): void {
|
||||
event.preventDefault();
|
||||
if (!this.draggingFromSquare) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromSquare = this.draggingFromSquare;
|
||||
this.clearDragState();
|
||||
|
||||
if (fromSquare === square.coordinate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.suppressNextClick = true;
|
||||
this.squareSelected.emit(square.coordinate);
|
||||
}
|
||||
|
||||
onSquareDragEnd(): void {
|
||||
this.clearDragState();
|
||||
}
|
||||
|
||||
isSelected(square: BoardSquare): boolean {
|
||||
return this.selectedSquare === square.coordinate;
|
||||
}
|
||||
@@ -49,6 +107,14 @@ export class ChessBoardComponent implements OnChanges {
|
||||
return this.highlightedSquareSet.has(square.coordinate);
|
||||
}
|
||||
|
||||
isDraggingSource(square: BoardSquare): boolean {
|
||||
return this.draggingFromSquare === square.coordinate;
|
||||
}
|
||||
|
||||
isDragOver(square: BoardSquare): boolean {
|
||||
return this.dragOverSquare === square.coordinate;
|
||||
}
|
||||
|
||||
private buildSquares(fen: string): BoardSquare[] {
|
||||
const placement = fen.split(' ')[0] ?? '';
|
||||
const rows = placement.split('/');
|
||||
@@ -87,4 +153,9 @@ export class ChessBoardComponent implements OnChanges {
|
||||
pieceCode
|
||||
};
|
||||
}
|
||||
|
||||
private clearDragState(): void {
|
||||
this.draggingFromSquare = null;
|
||||
this.dragOverSquare = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
.piece {
|
||||
width: clamp(40px, 9cqh, 130px);
|
||||
height: clamp(40px, 9cqh, 130px);
|
||||
width: clamp(50px, 11cqh, 160px);
|
||||
height: clamp(40px, 8cqh, 120px);
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.piece[draggable='true'] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.piece[draggable='true']:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.piece {
|
||||
width: clamp(40px, 9cqh, 130px);
|
||||
height: clamp(40px, 9cqh, 130px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.piece {
|
||||
width: clamp(32px, 8cqh, 100px);
|
||||
height: clamp(32px, 8cqh, 100px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.piece {
|
||||
width: clamp(24px, 6cqh, 75px);
|
||||
height: clamp(24px, 6cqh, 75px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
@if (pieceCode) {
|
||||
<img class="piece" [src]="spriteUrl" [alt]="pieceCode" />
|
||||
<img
|
||||
class="piece"
|
||||
[src]="spriteUrl"
|
||||
[alt]="pieceCode"
|
||||
[attr.draggable]="draggable ? 'true' : null"
|
||||
(dragstart)="onDragStart($event)"
|
||||
(dragend)="onDragEnd()"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
type BoardTheme = 'arabian' | 'classic';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chess-piece',
|
||||
@@ -8,18 +10,44 @@ import { Component, Input } from '@angular/core';
|
||||
})
|
||||
export class ChessPieceComponent {
|
||||
@Input({ required: true }) pieceCode: string | null = null;
|
||||
@Input() boardTheme: BoardTheme = 'arabian';
|
||||
@Input() draggable = false;
|
||||
@Output() pieceDragStart = new EventEmitter<DragEvent>();
|
||||
@Output() pieceDragEnd = new EventEmitter<void>();
|
||||
|
||||
onDragStart(event: DragEvent): void {
|
||||
if (!this.draggable) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pieceDragStart.emit(event);
|
||||
}
|
||||
|
||||
onDragEnd(): void {
|
||||
this.pieceDragEnd.emit();
|
||||
}
|
||||
|
||||
get spriteUrl(): string {
|
||||
if (!this.pieceCode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const color = this.pieceCode === this.pieceCode.toUpperCase() ? 'white' : 'black';
|
||||
const pieceName = this.getPieceName(this.pieceCode.toLowerCase());
|
||||
return `/arabian-chess/sprites/pieces/${color}_${pieceName}.png`;
|
||||
const isWhite = this.pieceCode === this.pieceCode.toUpperCase();
|
||||
const pieceCode = this.pieceCode.toLowerCase();
|
||||
|
||||
if (this.boardTheme === 'classic') {
|
||||
const colorPrefix = isWhite ? 'w' : 'b';
|
||||
const classicPieceName = this.getClassicPieceName(pieceCode);
|
||||
return `/assets/ChessAssets/${colorPrefix}_${classicPieceName}.png`;
|
||||
}
|
||||
|
||||
const arabianColor = isWhite ? 'white' : 'black';
|
||||
const arabianPieceName = this.getArabianPieceName(pieceCode);
|
||||
return `/assets/arabian-chess/sprites/pieces/${arabianColor}_${arabianPieceName}.png`;
|
||||
}
|
||||
|
||||
private getPieceName(piece: string): string {
|
||||
private getArabianPieceName(piece: string): string {
|
||||
switch (piece) {
|
||||
case 'k':
|
||||
return 'king';
|
||||
@@ -37,4 +65,23 @@ export class ChessPieceComponent {
|
||||
return 'pawn';
|
||||
}
|
||||
}
|
||||
|
||||
private getClassicPieceName(piece: string): string {
|
||||
switch (piece) {
|
||||
case 'k':
|
||||
return 'King';
|
||||
case 'q':
|
||||
return 'Queen';
|
||||
case 'r':
|
||||
return 'Rook';
|
||||
case 'b':
|
||||
return 'Bishop';
|
||||
case 'n':
|
||||
return 'Knight';
|
||||
case 'p':
|
||||
return 'Pawn';
|
||||
default:
|
||||
return 'Pawn';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
@import '../../button-template.css';
|
||||
|
||||
.input-card {
|
||||
background: var(--color-bg-card);
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--size-lg-padding);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-lg-gap);
|
||||
}
|
||||
|
||||
.move-card {
|
||||
padding: var(--size-sm-padding);
|
||||
gap: var(--size-sm-gap);
|
||||
}
|
||||
|
||||
.move-card input {
|
||||
min-height: 45px;
|
||||
padding: var(--size-md-padding);
|
||||
}
|
||||
|
||||
.input-card label {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--size-xs);
|
||||
}
|
||||
|
||||
.input-card textarea {
|
||||
resize: vertical;
|
||||
height: 40px;
|
||||
background-color:lightcyan;
|
||||
border: 3px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.input-card input {
|
||||
min-width: unset;
|
||||
background-color:lightcyan;
|
||||
border: 3px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<section [ngClass]="['input-card', cardClass]">
|
||||
<label>{{ label }}</label>
|
||||
|
||||
@if (inputType === 'textarea') {
|
||||
<textarea #textareaInput [placeholder]="placeholder" [value]="value" [rows]="rows"
|
||||
(input)="onValueChange(textareaInput.value)" class="form-input"></textarea>
|
||||
} @else {
|
||||
<input #textInput type="text" [placeholder]="placeholder" [value]="value" (input)="onValueChange(textInput.value)"
|
||||
class="form-input" />
|
||||
}
|
||||
|
||||
<button type="button" class="app-btn w-100" (click)="onButtonClick()">
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
|
||||
@if (hintText) {
|
||||
<p class="hint-text">{{ hintText }}</p>
|
||||
}
|
||||
</section>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './input-card.component.html',
|
||||
styleUrl: './input-card.component.css',
|
||||
})
|
||||
export class InputCardComponent {
|
||||
@Input() label: string = '';
|
||||
@Input() placeholder: string = '';
|
||||
@Input() buttonLabel: string = 'Send';
|
||||
@Input() inputType: 'input' | 'textarea' = 'input';
|
||||
@Input() value: string = '';
|
||||
@Input() cardClass: string = '';
|
||||
@Input() hintText: string = '';
|
||||
@Input() rows: number = 4;
|
||||
|
||||
@Output() valueChange = new EventEmitter<string>();
|
||||
@Output() buttonClick = new EventEmitter<void>();
|
||||
|
||||
onValueChange(newValue: string): void {
|
||||
this.value = newValue;
|
||||
this.valueChange.emit(newValue);
|
||||
}
|
||||
|
||||
onButtonClick(): void {
|
||||
this.buttonClick.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
@import '../../button-template.css';
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(2, 2, 10, 0.58);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 350;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
width: min(460px, 100%);
|
||||
background: var(--dlg-bg);
|
||||
border: 1.5px solid var(--dlg-border);
|
||||
box-shadow: var(--bb-glow);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-family: 'Bebas Neue', sans-serif;
|
||||
font-size: 22px;
|
||||
letter-spacing: 2px;
|
||||
color: var(--bb-title);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-input {
|
||||
width: 100%;
|
||||
background: rgba(4, 4, 20, 0.62);
|
||||
border: 1px solid var(--bb-border);
|
||||
color: var(--bb-title);
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 0.7rem;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dialog-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(0, 213, 255, 0.25);
|
||||
}
|
||||
|
||||
.dialog-textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="dialog-overlay" (click)="closeDialog()">
|
||||
<div class="dialog-card" (click)="$event.stopPropagation()">
|
||||
<div class="dialog-title">LOGIN</div>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
||||
<label for="username" class="sr-only">Username</label>
|
||||
<input id="username" type="text" class="dialog-input" formControlName="username" placeholder="Username"
|
||||
[disabled]="isLoading" />
|
||||
@if (loginForm.get('username')?.invalid && loginForm.get('username')?.touched) {
|
||||
<small class="text-danger">Username must be at least 3 characters</small>
|
||||
}
|
||||
|
||||
<label for="password" class="sr-only">Password</label>
|
||||
<input id="password" type="password" class="dialog-input" formControlName="password" placeholder="Password"
|
||||
[disabled]="isLoading" />
|
||||
@if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {
|
||||
<small class="text-danger">Password must be at least 6 characters</small>
|
||||
}
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-banner">{{ errorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="app-btn" (click)="openRegister()">Create account</button>
|
||||
<button type="button" class="app-btn" (click)="closeDialog()">Cancel</button>
|
||||
<button type="submit" class="app-btn" [disabled]="isLoading || loginForm.invalid">
|
||||
@if (isLoading) {
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
}
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Component, EventEmitter, Output, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { AuthDialogService } from '../../services/auth-dialog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './login-dialog.component.html',
|
||||
styleUrl: './login-dialog.component.css'
|
||||
})
|
||||
export class LoginDialogComponent {
|
||||
@Output() onClose = new EventEmitter<void>();
|
||||
@Output() onSuccess = new EventEmitter<void>();
|
||||
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly authDialogService = inject(AuthDialogService);
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
|
||||
loginForm: FormGroup;
|
||||
errorMessage: string | null = null;
|
||||
isLoading = false;
|
||||
|
||||
constructor() {
|
||||
this.loginForm = this.formBuilder.group({
|
||||
username: ['', [Validators.required, Validators.minLength(3)]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]]
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.loginForm.invalid) {
|
||||
this.errorMessage = 'Please fill in all fields correctly';
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
|
||||
const { username, password } = this.loginForm.value;
|
||||
this.authService.login(username, password).subscribe({
|
||||
next: () => {
|
||||
this.isLoading = false;
|
||||
this.onSuccess.emit();
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = err.error?.message || 'Login failed. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.onClose.emit();
|
||||
}
|
||||
|
||||
openRegister(): void {
|
||||
this.authDialogService.openRegister();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
@import '../../button-template.css';
|
||||
|
||||
.promotion-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
z-index: 1000;
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.promotion-dialog {
|
||||
background: var(--dlg-bg, white);
|
||||
border: 1.5px solid var(--dlg-border, #ddd);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--bb-glow, 0 4px 16px rgba(0, 0, 0, 0.2));
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.promotion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--bb-border, #e0e0e0);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-family: 'Bebas Neue', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--bb-title, #333);
|
||||
}
|
||||
}
|
||||
|
||||
.promotion-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.promotion-prompt {
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
color: var(--bb-title);
|
||||
opacity: 0.8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.promotion-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="promotion-dialog-overlay" [class.open]="isOpen">
|
||||
<div class="promotion-dialog">
|
||||
<div class="promotion-header">
|
||||
<h3>Pawn Promotion</h3>
|
||||
<button class="app-btn" (click)="close()" aria-label="Close"
|
||||
style="padding:0.2rem 0.5rem;min-width:unset;">×</button>
|
||||
</div>
|
||||
|
||||
<div class="promotion-body">
|
||||
<p class="promotion-prompt">Choose a piece to promote your pawn to:</p>
|
||||
|
||||
<div class="promotion-options">
|
||||
@for (piece of promotionPieces; track piece.type) {
|
||||
<button type="button" class="app-btn promotion-choice" [attr.data-piece]="piece.type"
|
||||
(click)="selectPromotion(piece.type)" [title]="piece.label">
|
||||
<span class="piece-symbol">{{ piece.symbol }}</span>
|
||||
<span class="piece-label">{{ piece.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type PromotionPieceType = 'queen' | 'rook' | 'bishop' | 'knight';
|
||||
interface PromotionPieceOption {
|
||||
type: PromotionPieceType;
|
||||
label: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-promotion-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './promotion-dialog.component.html',
|
||||
styleUrl: './promotion-dialog.component.css'
|
||||
})
|
||||
export class PromotionDialogComponent {
|
||||
@Input() isOpen = false;
|
||||
@Output() promotionSelected = new EventEmitter<PromotionPieceType>();
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
|
||||
promotionPieces: PromotionPieceOption[] = [
|
||||
{ type: 'queen', label: 'Queen', symbol: '♕' },
|
||||
{ type: 'rook', label: 'Rook', symbol: '♖' },
|
||||
{ type: 'bishop', label: 'Bishop', symbol: '♗' },
|
||||
{ type: 'knight', label: 'Knight', symbol: '♘' }
|
||||
];
|
||||
|
||||
selectPromotion(type: PromotionPieceType): void {
|
||||
this.promotionSelected.emit(type);
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed.emit();
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
@import '../../button-template.css';
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(2, 2, 10, 0.58);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 350;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
width: min(460px, 100%);
|
||||
background: var(--dlg-bg);
|
||||
border: 1.5px solid var(--dlg-border);
|
||||
box-shadow: var(--bb-glow);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-family: 'Bebas Neue', sans-serif;
|
||||
font-size: 22px;
|
||||
letter-spacing: 2px;
|
||||
color: var(--bb-title);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-input {
|
||||
width: 100%;
|
||||
background: rgba(4, 4, 20, 0.62);
|
||||
border: 1px solid var(--bb-border);
|
||||
color: var(--bb-title);
|
||||
border-radius: 2px;
|
||||
padding: 0.6rem 0.7rem;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dialog-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(0, 213, 255, 0.25);
|
||||
}
|
||||
|
||||
.dialog-textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<div class="dialog-overlay" (click)="closeDialog()">
|
||||
<div class="dialog-card" (click)="$event.stopPropagation()">
|
||||
<div class="dialog-title">CREATE ACCOUNT</div>
|
||||
|
||||
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
|
||||
<input id="username" type="text" class="dialog-input" formControlName="username" placeholder="Username"
|
||||
[disabled]="isLoading" />
|
||||
@if (registerForm.get('username')?.invalid && registerForm.get('username')?.touched) {
|
||||
<small class="text-danger">Username must be at least 3 characters</small>
|
||||
}
|
||||
|
||||
<input id="email" type="email" class="dialog-input" formControlName="email" placeholder="Email"
|
||||
[disabled]="isLoading" />
|
||||
@if (registerForm.get('email')?.invalid && registerForm.get('email')?.touched) {
|
||||
<small class="text-danger">Please enter a valid email</small>
|
||||
}
|
||||
|
||||
<input id="password" type="password" class="dialog-input" formControlName="password" placeholder="Password"
|
||||
[disabled]="isLoading" />
|
||||
@if (registerForm.get('password')?.invalid && registerForm.get('password')?.touched) {
|
||||
<small class="text-danger">Password must be at least 6 characters</small>
|
||||
}
|
||||
|
||||
<input id="confirmPassword" type="password" class="dialog-input" formControlName="confirmPassword"
|
||||
placeholder="Confirm Password" [disabled]="isLoading" />
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-banner">{{ errorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="app-btn" (click)="openLogin()">Already have an account?</button>
|
||||
<button type="button" class="app-btn" (click)="closeDialog()">Cancel</button>
|
||||
<button type="submit" class="app-btn" [disabled]="isLoading || registerForm.invalid">
|
||||
@if (isLoading) {
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
}
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component, EventEmitter, Output, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { AuthDialogService } from '../../services/auth-dialog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './register-dialog.component.html',
|
||||
styleUrl: './register-dialog.component.css'
|
||||
})
|
||||
export class RegisterDialogComponent {
|
||||
@Output() onClose = new EventEmitter<void>();
|
||||
@Output() onSuccess = new EventEmitter<void>();
|
||||
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly authDialogService = inject(AuthDialogService);
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
|
||||
registerForm: FormGroup;
|
||||
errorMessage: string | null = null;
|
||||
isLoading = false;
|
||||
|
||||
constructor() {
|
||||
this.registerForm = this.formBuilder.group({
|
||||
username: ['', [Validators.required, Validators.minLength(3)]],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
confirmPassword: ['', [Validators.required]]
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.registerForm.invalid) {
|
||||
this.errorMessage = 'Please fill in all fields correctly';
|
||||
return;
|
||||
}
|
||||
|
||||
const { password, confirmPassword } = this.registerForm.value;
|
||||
if (password !== confirmPassword) {
|
||||
this.errorMessage = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = null;
|
||||
|
||||
const { username, email, password: pwd } = this.registerForm.value;
|
||||
this.authService.register(username, pwd, email).subscribe({
|
||||
next: () => {
|
||||
this.isLoading = false;
|
||||
this.onSuccess.emit();
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage =
|
||||
err.error?.message || 'Registration failed. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.onClose.emit();
|
||||
}
|
||||
|
||||
openLogin(): void {
|
||||
this.authDialogService.openLogin();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
@import '../../button-template.css';
|
||||
|
||||
.navbar {
|
||||
background: rgba(8, 6, 28, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 4px 20px rgba(0, 210, 255, 0.15);
|
||||
border-bottom: 1px solid rgba(0, 210, 255, 0.2);
|
||||
border-radius: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--bb-title) !important;
|
||||
font-family: 'Bebas Neue', sans-serif;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.me-btn {
|
||||
background: rgba(0, 210, 255, 0.1);
|
||||
color: var(--bb-title);
|
||||
border: 1px solid var(--bb-border);
|
||||
border-radius: 2px;
|
||||
padding: 0.5rem 0.8rem;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.me-btn:hover {
|
||||
background: rgba(0, 210, 255, 0.2);
|
||||
border-color: var(--bb-tag);
|
||||
box-shadow: 0 0 10px rgba(0, 210, 255, 0.4);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.me-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Sunset Mode */
|
||||
.sunset .navbar {
|
||||
background: rgba(20, 5, 45, 0.85);
|
||||
border-bottom-color: rgba(255, 64, 207, 0.2);
|
||||
box-shadow: 0 4px 20px rgba(242, 106, 226, 0.15);
|
||||
}
|
||||
|
||||
.sunset .me-btn {
|
||||
background: rgba(242, 106, 226, 0.1);
|
||||
border-color: var(--bb-border);
|
||||
}
|
||||
|
||||
.sunset .me-btn:hover {
|
||||
background: rgba(242, 106, 226, 0.2);
|
||||
border-color: var(--bb-tag);
|
||||
box-shadow: 0 0 10px rgba(242, 106, 226, 0.4);
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ms-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<nav class="navbar">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand">NowChess</span>
|
||||
<div class="ms-auto">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" class="app-btn" (click)="toggleTheme()">
|
||||
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
|
||||
</button>
|
||||
@if (currentUser; as user) {
|
||||
<div class="d-flex align-items-center gap-2 user-section">
|
||||
<button type="button" class="me-btn" (click)="goToProfile()">
|
||||
👤 {{ user.username }}
|
||||
</button>
|
||||
<button type="button" class="app-btn" (click)="logout()">Logout</button>
|
||||
</div>
|
||||
} @else {
|
||||
<button type="button" class="app-btn" (click)="openLoginDialog()">
|
||||
Login
|
||||
</button>
|
||||
<button type="button" class="app-btn" (click)="openRegisterDialog()">
|
||||
Register
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@if (showLoginDialog) {
|
||||
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
|
||||
}
|
||||
|
||||
@if (showRegisterDialog) {
|
||||
<app-register-dialog (onClose)="closeRegisterDialog()" (onSuccess)="onRegisterSuccess()" />
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { AuthDialogService } from '../../services/auth-dialog.service';
|
||||
import { CurrentUser } from '../../models/auth.models';
|
||||
import { LoginDialogComponent } from '../login-dialog/login-dialog.component';
|
||||
import { RegisterDialogComponent } from '../register-dialog/register-dialog.component';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toolbar',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent],
|
||||
templateUrl: './toolbar.component.html',
|
||||
styleUrl: './toolbar.component.css'
|
||||
})
|
||||
export class ToolbarComponent implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly authDialogService = inject(AuthDialogService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
currentUser: CurrentUser | null = null;
|
||||
showLoginDialog = false;
|
||||
showRegisterDialog = false;
|
||||
isDarkMode = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.authService.currentUser$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((user) => {
|
||||
this.currentUser = user;
|
||||
});
|
||||
|
||||
this.authDialogService.dialogState$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((state) => {
|
||||
this.showLoginDialog = state === 'login';
|
||||
this.showRegisterDialog = state === 'register';
|
||||
});
|
||||
|
||||
this.themeService.darkMode$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((isDarkMode) => {
|
||||
this.isDarkMode = isDarkMode;
|
||||
});
|
||||
}
|
||||
|
||||
openLoginDialog(): void {
|
||||
this.authDialogService.openLogin();
|
||||
}
|
||||
|
||||
closeLoginDialog(): void {
|
||||
this.authDialogService.close();
|
||||
}
|
||||
|
||||
openRegisterDialog(): void {
|
||||
this.authDialogService.openRegister();
|
||||
}
|
||||
|
||||
closeRegisterDialog(): void {
|
||||
this.authDialogService.close();
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
toggleTheme(): void {
|
||||
this.themeService.toggleTheme();
|
||||
}
|
||||
|
||||
goToProfile(): void {
|
||||
this.router.navigate(['/profile']);
|
||||
}
|
||||
|
||||
onLoginSuccess(): void {
|
||||
this.closeLoginDialog();
|
||||
}
|
||||
|
||||
onRegisterSuccess(): void {
|
||||
this.closeRegisterDialog();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { GameTurn } from '../../models/game.models';
|
||||
|
||||
export function getPieceAtSquare(fen: string, targetSquare: string): string | null {
|
||||
const placement = fen.split(' ')[0] ?? '';
|
||||
const rows = placement.split('/');
|
||||
if (rows.length !== 8 || targetSquare.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const file = targetSquare.charCodeAt(0) - 97;
|
||||
const rank = Number(targetSquare[1]);
|
||||
const rowIndex = 8 - rank;
|
||||
|
||||
if (Number.isNaN(rank) || file < 0 || file > 7 || rowIndex < 0 || rowIndex > 7) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let column = 0;
|
||||
for (const symbol of rows[rowIndex]) {
|
||||
if (symbol >= '1' && symbol <= '8') {
|
||||
column += Number(symbol);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (column === file) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
column += 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isPieceColor(pieceCode: string, turn: GameTurn): boolean {
|
||||
const isWhitePiece = pieceCode === pieceCode.toUpperCase();
|
||||
return (turn === 'white' && isWhitePiece) || (turn === 'black' && !isWhitePiece);
|
||||
}
|
||||