11 Commits

Author SHA1 Message Date
TeamCity 3c1f5c76e0 ci: bump version to v0.1.0 2026-05-12 16:33:23 +00:00
shosho996 36d72fd6cd fix: build issues 2026-05-12 17:33:03 +02:00
shosho996 bd7ec581e3 feat: NCS-75 Frontend Deployment Dockerfile (#4)
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #4
Co-authored-by: Shahd Lala <shosho996@blackhole.local>
Co-committed-by: Shahd Lala <shosho996@blackhole.local>
2026-05-12 11:21:04 +02:00
shosho996 ff75c8ce2f feat: NCS-63 User account implementation (#2)
User Profile info, no game before login/register, menu bar

---------

Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de>
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #2
2026-05-06 10:51:30 +02:00
shosho996 2de003e497 feat: added bot, light and dark mode 2026-04-22 10:22:22 +02:00
shahdlala66 e83ec814d9 style: removed comments 2026-04-18 23:45:08 +02:00
shahdlala66 0d7bb0343c style: bootstrap added 2026-04-18 23:21:25 +02:00
shahdlala66 8b090e4d96 fix: cleaner components seperation 2026-04-18 21:39:16 +02:00
shahdlala66 3e8c7c4057 fix: structure 2026-04-18 11:36:14 +02:00
shahdlala66 4da882f481 fix: gitignore 2026-04-18 09:40:48 +02:00
shahdlala66 8df2d0550a fix: GITIGNORE 2026-04-18 01:07:45 +02:00
132 changed files with 5903 additions and 766 deletions
+2 -2
View File
@@ -21,9 +21,9 @@ yarn-error.log
*.sublime-workspace *.sublime-workspace
# Visual Studio Code # Visual Studio Code
.vscode/* .vscode/
.history/* .history/*
.angualar/ .angular/
# Miscellaneous # Miscellaneous
/.angular/cache /.angular/cache
.sass-cache/ .sass-cache/
-4
View File
@@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}
-20
View File
@@ -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"
}
]
}
-42
View File
@@ -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"
}
}
}
}
]
}
+20
View File
@@ -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))
+25
View File
@@ -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"]
+29 -48
View File
@@ -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 ```bash
npm install npm install
npm start 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 ```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.
+42 -9
View File
@@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"cli": { "cli": {
"packageManager": "npm" "packageManager": "npm",
"analytics": false
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
@@ -28,8 +29,13 @@
}, },
{ {
"glob": "**/*", "glob": "**/*",
"input": "ARABIAN CHESS", "input": "assets/arabian-chess",
"output": "/arabian-chess" "output": "/assets/arabian-chess"
},
{
"glob": "**/*",
"input": "assets/ChessAssets",
"output": "/assets/ChessAssets"
} }
], ],
"styles": [ "styles": [
@@ -41,18 +47,32 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kB", "maximumWarning": "1MB",
"maximumError": "1MB" "maximumError": "5MB"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "4kB", "maximumWarning": "1MB",
"maximumError": "8kB" "maximumError": "5MB"
} }
], ],
"outputHashing": "all" "outputHashing": "all"
}, },
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
},
"development": { "development": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
],
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
@@ -62,10 +82,18 @@
}, },
"serve": { "serve": {
"builder": "@angular/build:dev-server", "builder": "@angular/build:dev-server",
"options": {
"proxyConfig": "proxy.conf.json",
"host": "0.0.0.0",
"port": 4200
},
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "nowchess-frontend:build:production" "buildTarget": "nowchess-frontend:build:production"
}, },
"staging": {
"buildTarget": "nowchess-frontend:build:staging"
},
"development": { "development": {
"buildTarget": "nowchess-frontend:build:development" "buildTarget": "nowchess-frontend:build:development"
} }
@@ -90,8 +118,13 @@
}, },
{ {
"glob": "**/*", "glob": "**/*",
"input": "ARABIAN CHESS", "input": "assets/arabian-chess",
"output": "/arabian-chess" "output": "/assets/arabian-chess"
},
{
"glob": "**/*",
"input": "assets/ChessAssets",
"output": "/assets/ChessAssets"
} }
], ],
"styles": [ "styles": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+18
View File
@@ -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 openapi: 3.0.3
info: info:
title: NowChess API title: NowChess Board API
description: | description: |
REST API for the NowChess application. Designed to feel familiar to users REST API for the NowChess application. Designed to feel familiar to users
of the [lichess API](https://lichess.org/api). of the [lichess API](https://lichess.org/api).
@@ -186,11 +186,8 @@ paths:
currently to move. currently to move.
For promotion moves include the target piece as the fifth character: For promotion moves include the target piece as the fifth character:
`e7e8q`, `a2a1r`, etc. `e7e8q`, `a2a1r`, etc. Promotion moves without the fifth character
are rejected with `400 INVALID_MOVE`.
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.
security: security:
- bearerAuth: [] - bearerAuth: []
parameters: parameters:
@@ -630,7 +627,6 @@ components:
| `draw` | Draw agreed or claimed — game over | | `draw` | Draw agreed or claimed — game over |
| `drawOffered` | Waiting for the opponent to accept or decline a draw offer | | `drawOffered` | Waiting for the opponent to accept or decline a draw offer |
| `fiftyMoveAvailable` | Fifty-move rule threshold reached; active player may claim draw | | `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) | | `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) |
enum: enum:
- started - started
@@ -641,7 +637,6 @@ components:
- draw - draw
- drawOffered - drawOffered
- fiftyMoveAvailable - fiftyMoveAvailable
- promotionPending
- insufficientMaterial - insufficientMaterial
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
+22
View File
@@ -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;
}
}
+34 -24
View File
@@ -14,6 +14,7 @@
"@angular/forms": "^20.3.0", "@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"bootstrap": "^5.3.8",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
@@ -457,7 +458,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz",
"integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==", "integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -474,7 +474,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz",
"integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==", "integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -488,7 +487,6 @@
"integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==", "integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -521,7 +519,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz",
"integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==", "integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -565,7 +562,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz",
"integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==", "integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -632,7 +628,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@@ -1606,7 +1601,6 @@
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@inquirer/checkbox": "^4.2.1", "@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14", "@inquirer/confirm": "^5.1.14",
@@ -2969,6 +2963,17 @@
"license": "MIT", "license": "MIT",
"optional": true "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": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", "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==", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.19.0" "undici-types": "~7.19.0"
} }
@@ -3820,6 +3824,25 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/brace-expansion": {
"version": "1.1.14", "version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
@@ -3864,7 +3887,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001782",
@@ -4880,7 +4902,6 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -5322,7 +5343,6 @@
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -5819,8 +5839,7 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz",
"integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/jose": { "node_modules/jose": {
"version": "6.2.2", "version": "6.2.2",
@@ -5922,7 +5941,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "1.5.0", "@colors/colors": "1.5.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@@ -6390,7 +6408,6 @@
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^4.0.0",
"colorette": "^2.0.20", "colorette": "^2.0.20",
@@ -7903,7 +7920,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@@ -7939,7 +7955,6 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@@ -8559,8 +8574,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD", "license": "0BSD"
"peer": true
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "4.1.0", "version": "4.1.0",
@@ -8598,7 +8612,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -8728,7 +8741,6 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -9526,7 +9538,6 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -9545,8 +9556,7 @@
"version": "0.15.1", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT", "license": "MIT"
"peer": true
} }
} }
} }
+4 -2
View File
@@ -4,7 +4,8 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build --configuration production",
"build:staging": "ng build --configuration staging",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test" "test": "ng test"
}, },
@@ -28,6 +29,7 @@
"@angular/forms": "^20.3.0", "@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"bootstrap": "^5.3.8",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
@@ -45,4 +47,4 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.9.2" "typescript": "~5.9.2"
} }
} }
+18
View File
@@ -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
}
}
+4
View File
@@ -0,0 +1,4 @@
window.__RUNTIME_CONFIG__ = {
API_URL: "${API_URL}",
WEBSOCKET_URL: "${WEBSOCKET_URL}"
};
+3 -2
View File
@@ -1,14 +1,15 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; 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 { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { authInterceptor } from './services/auth.interceptor';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(), provideHttpClient(withInterceptors([authInterceptor])),
provideRouter(routes) provideRouter(routes)
] ]
}; };
+2 -1
View File
@@ -1 +1,2 @@
<router-outlet /> <app-toolbar />
<router-outlet />
+2
View File
@@ -1,9 +1,11 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { GameComponent } from './pages/game/game.component'; import { GameComponent } from './pages/game/game.component';
import { WelcomeComponent } from './pages/welcome/welcome.component'; import { WelcomeComponent } from './pages/welcome/welcome.component';
import { ProfileComponent } from './pages/profile/profile.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: WelcomeComponent }, { path: '', component: WelcomeComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'game/:gameId', component: GameComponent }, { path: 'game/:gameId', component: GameComponent },
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' }
]; ];
+10 -3
View File
@@ -1,11 +1,18 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { ToolbarComponent } from './components/toolbar/toolbar.component';
import { ThemeService } from './services/theme.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet, ToolbarComponent],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.css' styleUrl: './app.css'
}) })
export class App { export class App implements OnInit {
constructor(private readonly themeService: ThemeService) { }
ngOnInit(): void {
this.themeService.initTheme();
}
} }
+69
View File
@@ -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 { .board-grid {
display: grid; display: grid;
grid-template-columns: repeat(8, 1fr); grid-template-columns: repeat(8, 1fr);
border: 2px solid #5A2C28; border: var(--border-width) solid var(--color-border);
border-radius: 10px 10px 0 0; border-radius: var(--border-radius-md) var(--border-radius-md) 0 0;
overflow: hidden; overflow: hidden;
background: #5A2C28; background: var(--color-text-primary);
} }
.square { .square {
@@ -27,12 +27,56 @@
cursor: pointer; 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 { .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 { .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 { .square.highlighted::after {
@@ -41,12 +85,13 @@
width: 28%; width: 28%;
height: 28%; height: 28%;
border-radius: 50%; border-radius: 50%;
background: rgba(193, 158, 245, 0.75); background: var(--color-secondary-purple);
border: 2px solid #5A2C28; opacity: 0.75;
border: var(--border-width) solid var(--color-border);
} }
.square.selected { .square.selected {
outline: 3px solid #BA6D4B; outline: 3px solid var(--color-primary);
outline-offset: -3px; outline-offset: -3px;
} }
@@ -55,7 +100,7 @@
width: 100%; width: 100%;
display: block; display: block;
object-fit: fill; object-fit: fill;
border: 2px solid #5A2C28; border: var(--border-width) solid var(--color-border);
border-top: 0; 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-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)) { @for (square of squares; track trackByCoordinate($index, square)) {
<button <button
type="button" type="button"
@@ -8,13 +8,25 @@
[class.dark]="!square.isLight" [class.dark]="!square.isLight"
[class.selected]="isSelected(square)" [class.selected]="isSelected(square)"
[class.highlighted]="isHighlighted(square)" [class.highlighted]="isHighlighted(square)"
[class.drag-source]="isDraggingSource(square)"
[class.drag-over]="isDragOver(square)"
[attr.data-square]="square.coordinate" [attr.data-square]="square.coordinate"
(click)="onSquareClick(square)" (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> </button>
} }
</div> </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> </div>
@@ -7,6 +7,8 @@ interface BoardSquare {
pieceCode: string | null; pieceCode: string | null;
} }
type BoardTheme = 'arabian' | 'classic';
@Component({ @Component({
selector: 'app-chess-board', selector: 'app-chess-board',
standalone: true, standalone: true,
@@ -18,10 +20,14 @@ export class ChessBoardComponent implements OnChanges {
@Input({ required: true }) fen = ''; @Input({ required: true }) fen = '';
@Input() selectedSquare: string | null = null; @Input() selectedSquare: string | null = null;
@Input() highlightedSquares: string[] = []; @Input() highlightedSquares: string[] = [];
@Input() boardTheme: BoardTheme = 'arabian';
@Output() squareSelected = new EventEmitter<string>(); @Output() squareSelected = new EventEmitter<string>();
squares: BoardSquare[] = []; squares: BoardSquare[] = [];
private highlightedSquareSet = new Set<string>(); private highlightedSquareSet = new Set<string>();
private draggingFromSquare: string | null = null;
private dragOverSquare: string | null = null;
private suppressNextClick = false;
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes['fen']) { if (changes['fen']) {
@@ -38,9 +44,61 @@ export class ChessBoardComponent implements OnChanges {
} }
onSquareClick(square: BoardSquare): void { onSquareClick(square: BoardSquare): void {
if (this.suppressNextClick) {
this.suppressNextClick = false;
return;
}
this.squareSelected.emit(square.coordinate); 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 { isSelected(square: BoardSquare): boolean {
return this.selectedSquare === square.coordinate; return this.selectedSquare === square.coordinate;
} }
@@ -49,6 +107,14 @@ export class ChessBoardComponent implements OnChanges {
return this.highlightedSquareSet.has(square.coordinate); 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[] { private buildSquares(fen: string): BoardSquare[] {
const placement = fen.split(' ')[0] ?? ''; const placement = fen.split(' ')[0] ?? '';
const rows = placement.split('/'); const rows = placement.split('/');
@@ -87,4 +153,9 @@ export class ChessBoardComponent implements OnChanges {
pieceCode pieceCode
}; };
} }
private clearDragState(): void {
this.draggingFromSquare = null;
this.dragOverSquare = null;
}
} }
@@ -1,7 +1,36 @@
.piece { .piece {
width: clamp(40px, 9cqh, 130px); width: clamp(50px, 11cqh, 160px);
height: clamp(40px, 9cqh, 130px); height: clamp(40px, 8cqh, 120px);
display: block; display: block;
object-fit: contain; 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) { @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({ @Component({
selector: 'app-chess-piece', selector: 'app-chess-piece',
@@ -8,18 +10,44 @@ import { Component, Input } from '@angular/core';
}) })
export class ChessPieceComponent { export class ChessPieceComponent {
@Input({ required: true }) pieceCode: string | null = null; @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 { get spriteUrl(): string {
if (!this.pieceCode) { if (!this.pieceCode) {
return ''; return '';
} }
const color = this.pieceCode === this.pieceCode.toUpperCase() ? 'white' : 'black'; const isWhite = this.pieceCode === this.pieceCode.toUpperCase();
const pieceName = this.getPieceName(this.pieceCode.toLowerCase()); const pieceCode = this.pieceCode.toLowerCase();
return `/arabian-chess/sprites/pieces/${color}_${pieceName}.png`;
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) { switch (piece) {
case 'k': case 'k':
return 'king'; return 'king';
@@ -37,4 +65,23 @@ export class ChessPieceComponent {
return 'pawn'; 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;">&times;</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();
}
}
+38
View File
@@ -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);
}

Some files were not shown because too many files have changed in this diff Show More