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
|
*.sublime-workspace
|
||||||
|
|
||||||
# Visual Studio Code
|
# Visual Studio Code
|
||||||
.vscode/*
|
.vscode/
|
||||||
.history/*
|
.history/*
|
||||||
.angualar/
|
.angular/
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
/.angular/cache
|
/.angular/cache
|
||||||
.sass-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
|
```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.
|
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
|
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
|
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
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -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/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,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
<router-outlet />
|
<app-toolbar />
|
||||||
|
<router-outlet />
|
||||||
@@ -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: '' }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;">×</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);
|
||||||
|
}
|
||||||