From ff75c8ce2fad54137f04a14c15bc1d4a38fa9bb8 Mon Sep 17 00:00:00 2001 From: Shahd Lala Date: Wed, 6 May 2026 10:51:30 +0200 Subject: [PATCH] feat: NCS-63 User account implementation (#2) User Profile info, no game before login/register, menu bar --------- Co-authored-by: Lala, Shahd Co-authored-by: shahdlala66 Reviewed-on: https://git.janis-eccarius.de/NowChess/NowChess-Frontend/pulls/2 --- angular.json | 29 +- assets/ChessAssets/b_Bishop.png | Bin 0 -> 2606 bytes assets/ChessAssets/b_King.png | Bin 0 -> 2084 bytes assets/ChessAssets/b_Knight.png | Bin 0 -> 2260 bytes assets/ChessAssets/b_Pawn.png | Bin 0 -> 2268 bytes assets/ChessAssets/b_Queen.png | Bin 0 -> 2538 bytes assets/ChessAssets/b_Rook.png | Bin 0 -> 1611 bytes assets/ChessAssets/w_Bishop.png | Bin 0 -> 2747 bytes assets/ChessAssets/w_King.png | Bin 0 -> 2088 bytes assets/ChessAssets/w_Knight.png | Bin 0 -> 2289 bytes assets/ChessAssets/w_Pawn.png | Bin 0 -> 2390 bytes assets/ChessAssets/w_Queen.png | Bin 0 -> 2575 bytes assets/ChessAssets/w_Rook.png | Bin 0 -> 1689 bytes .../arabian-chess}/license.txt | 0 .../arabian-chess}/plane.png | Bin .../arabian-chess}/player-one.gif | Bin .../arabian-chess}/player-two.gif | Bin .../arabian-chess}/raf.gif | Bin .../arabian-chess}/ref/cover.png | Bin .../arabian-chess}/ref/full_art.png | Bin .../arabian-chess}/ref/logo.png | Bin .../arabian-chess}/sheets/board.png | Bin .../arabian-chess}/sheets/board_centered.png | Bin .../sheets/board_without_bottom.png | Bin .../arabian-chess}/sheets/nums & letters.png | Bin .../arabian-chess}/sheets/pieces.png | Bin .../sprites/board/board_bottom.png | Bin .../sprites/board/board_square_black.png | Bin .../sprites/board/board_square_white.png | Bin .../sprites/nums & letters/letter_a.png | Bin .../sprites/nums & letters/letter_b.png | Bin .../sprites/nums & letters/letter_c.png | Bin .../sprites/nums & letters/letter_d.png | Bin .../sprites/nums & letters/letter_e.png | Bin .../sprites/nums & letters/letter_f.png | Bin .../sprites/nums & letters/letter_g.png | Bin .../sprites/nums & letters/letter_h.png | Bin .../sprites/nums & letters/num_0.png | Bin .../sprites/nums & letters/num_1.png | Bin .../sprites/nums & letters/num_2.png | Bin .../sprites/nums & letters/num_3.png | Bin .../sprites/nums & letters/num_4.png | Bin .../sprites/nums & letters/num_5.png | Bin .../sprites/nums & letters/num_6.png | Bin .../sprites/nums & letters/num_7.png | Bin .../sprites/pieces/black_bishop.png | Bin .../sprites/pieces/black_king.png | Bin .../sprites/pieces/black_knight.png | Bin .../sprites/pieces/black_pawn.png | Bin .../sprites/pieces/black_queen.png | Bin .../sprites/pieces/black_rook.png | Bin .../sprites/pieces/white_bishop.png | Bin .../sprites/pieces/white_king.png | Bin .../sprites/pieces/white_knight.png | Bin .../sprites/pieces/white_pawn.png | Bin .../sprites/pieces/white_queen.png | Bin .../sprites/pieces/white_rook.png | Bin .../arabian-chess}/xav.png | Bin .../arabian-chess}/zayne.png | Bin package-lock.json | 27 +- proxy.conf.json | 10 + src/app/app.config.ts | 5 +- src/app/app.html | 3 +- src/app/app.routes.ts | 2 + src/app/app.ts | 15 +- src/app/button-template.css | 69 + .../chess-board/chess-board.component.css | 48 +- .../chess-board/chess-board.component.html | 18 +- .../chess-board/chess-board.component.ts | 71 + .../chess-piece/chess-piece.component.css | 10 +- .../chess-piece/chess-piece.component.html | 9 +- .../chess-piece/chess-piece.component.ts | 57 +- .../input-card/input-card.component.css | 18 +- .../input-card/input-card.component.html | 26 +- .../login-dialog/login-dialog.component.css | 68 + .../login-dialog/login-dialog.component.html | 36 + .../login-dialog/login-dialog.component.ts | 62 + .../promotion-dialog.component.css | 76 +- .../promotion-dialog.component.html | 23 +- .../register-dialog.component.css | 68 + .../register-dialog.component.html | 43 + .../register-dialog.component.ts | 71 + .../components/toolbar/toolbar.component.css | 84 + .../components/toolbar/toolbar.component.html | 35 + .../components/toolbar/toolbar.component.ts | 87 + src/app/models/auth.models.ts | 28 + src/app/pages/game/game.component.css | 216 +++ src/app/pages/game/game.component.html | 182 ++- src/app/pages/game/game.component.ts | 226 ++- src/app/pages/profile/profile.component.css | 478 ++++++ src/app/pages/profile/profile.component.html | 97 ++ src/app/pages/profile/profile.component.ts | 127 ++ src/app/pages/welcome/welcome.component.css | 1443 ++++++++++------- src/app/pages/welcome/welcome.component.html | 339 +++- src/app/pages/welcome/welcome.component.ts | 561 ++++++- src/app/services/auth-dialog.service.ts | 23 + src/app/services/auth.interceptor.ts | 22 + src/app/services/auth.service.ts | 93 ++ src/app/services/theme.service.ts | 34 + src/cityscape.html | 0 src/environments/environment.development.ts | 1 + src/environments/environment.ts | 1 + src/styles-variables.css | 18 +- src/styles.css | 251 +++ 104 files changed, 4232 insertions(+), 978 deletions(-) create mode 100644 assets/ChessAssets/b_Bishop.png create mode 100644 assets/ChessAssets/b_King.png create mode 100644 assets/ChessAssets/b_Knight.png create mode 100644 assets/ChessAssets/b_Pawn.png create mode 100644 assets/ChessAssets/b_Queen.png create mode 100644 assets/ChessAssets/b_Rook.png create mode 100644 assets/ChessAssets/w_Bishop.png create mode 100644 assets/ChessAssets/w_King.png create mode 100644 assets/ChessAssets/w_Knight.png create mode 100644 assets/ChessAssets/w_Pawn.png create mode 100644 assets/ChessAssets/w_Queen.png create mode 100644 assets/ChessAssets/w_Rook.png rename {arabian-chess => assets/arabian-chess}/license.txt (100%) rename {arabian-chess => assets/arabian-chess}/plane.png (100%) rename {arabian-chess => assets/arabian-chess}/player-one.gif (100%) rename {arabian-chess => assets/arabian-chess}/player-two.gif (100%) rename {arabian-chess => assets/arabian-chess}/raf.gif (100%) rename {arabian-chess => assets/arabian-chess}/ref/cover.png (100%) rename {arabian-chess => assets/arabian-chess}/ref/full_art.png (100%) rename {arabian-chess => assets/arabian-chess}/ref/logo.png (100%) rename {arabian-chess => assets/arabian-chess}/sheets/board.png (100%) rename {arabian-chess => assets/arabian-chess}/sheets/board_centered.png (100%) rename {arabian-chess => assets/arabian-chess}/sheets/board_without_bottom.png (100%) rename {arabian-chess => assets/arabian-chess}/sheets/nums & letters.png (100%) rename {arabian-chess => assets/arabian-chess}/sheets/pieces.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/board/board_bottom.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/board/board_square_black.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/board/board_square_white.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_a.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_b.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_c.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_d.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_e.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_f.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_g.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/letter_h.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_0.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_1.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_2.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_3.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_4.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_5.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_6.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/nums & letters/num_7.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/black_bishop.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/black_king.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/black_knight.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/black_pawn.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/black_queen.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/black_rook.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/white_bishop.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/white_king.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/white_knight.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/white_pawn.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/white_queen.png (100%) rename {arabian-chess => assets/arabian-chess}/sprites/pieces/white_rook.png (100%) rename {arabian-chess => assets/arabian-chess}/xav.png (100%) rename {arabian-chess => assets/arabian-chess}/zayne.png (100%) create mode 100644 src/app/button-template.css create mode 100644 src/app/components/login-dialog/login-dialog.component.css create mode 100644 src/app/components/login-dialog/login-dialog.component.html create mode 100644 src/app/components/login-dialog/login-dialog.component.ts create mode 100644 src/app/components/register-dialog/register-dialog.component.css create mode 100644 src/app/components/register-dialog/register-dialog.component.html create mode 100644 src/app/components/register-dialog/register-dialog.component.ts create mode 100644 src/app/components/toolbar/toolbar.component.css create mode 100644 src/app/components/toolbar/toolbar.component.html create mode 100644 src/app/components/toolbar/toolbar.component.ts create mode 100644 src/app/models/auth.models.ts create mode 100644 src/app/pages/profile/profile.component.css create mode 100644 src/app/pages/profile/profile.component.html create mode 100644 src/app/pages/profile/profile.component.ts create mode 100644 src/app/services/auth-dialog.service.ts create mode 100644 src/app/services/auth.interceptor.ts create mode 100644 src/app/services/auth.service.ts create mode 100644 src/app/services/theme.service.ts create mode 100644 src/cityscape.html diff --git a/angular.json b/angular.json index fd1f73a..aafdf64 100644 --- a/angular.json +++ b/angular.json @@ -2,7 +2,8 @@ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { - "packageManager": "npm" + "packageManager": "npm", + "analytics": false }, "newProjectRoot": "projects", "projects": { @@ -28,8 +29,13 @@ }, { "glob": "**/*", - "input": "arabian-chess", - "output": "/arabian-chess" + "input": "assets/arabian-chess", + "output": "/assets/arabian-chess" + }, + { + "glob": "**/*", + "input": "assets/ChessAssets", + "output": "/assets/ChessAssets" } ], "styles": [ @@ -46,8 +52,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "12kB", + "maximumError": "20kB" } ], "outputHashing": "all" @@ -69,7 +75,9 @@ "serve": { "builder": "@angular/build:dev-server", "options": { - "proxyConfig": "proxy.conf.json" + "proxyConfig": "proxy.conf.json", + "host": "0.0.0.0", + "port": 4200 }, "configurations": { "production": { @@ -99,8 +107,13 @@ }, { "glob": "**/*", - "input": "arabian-chess", - "output": "/arabian-chess" + "input": "assets/arabian-chess", + "output": "/assets/arabian-chess" + }, + { + "glob": "**/*", + "input": "assets/ChessAssets", + "output": "/assets/ChessAssets" } ], "styles": [ diff --git a/assets/ChessAssets/b_Bishop.png b/assets/ChessAssets/b_Bishop.png new file mode 100644 index 0000000000000000000000000000000000000000..26458d8fc12add45658730453f02aa03ada96546 GIT binary patch literal 2606 zcmV+}3eok6P)Jb9M)(U7Zrm#Vf}}TYloQ-YPlyxk3H20n z0*M1D;(!Dq1`2IOL(;T$693rceP`Dtt(rD=HlEp;_Yor}MuP0VH}Ac9Z{`h=AP9mW z2!bF8f*=TjAXrM3$HzR@c|^(hfyd`O0&P}MGh!|Pj2wh z^q4mvu<*U7s677NMA4&Whh`eBsK{SkBg=8Y0t9U8*I%8^A2{p@k28ek*J-@Z-B=IW8zbWu(x%p_>{}uV$VsM-<=yk`+Qgmg9l~2v5NU z1*r4do_gg$GsMRlS&j<|aAl*>y8`UA)Q2d51zf##zk?;+p=pPaN(fP$5wsc?5<&GS zqEaiQx$^Mg^!>)&khb?!O885%9H&yjTmz__nK(EVjG6x@3;)goNd?=+E{`uCG^651 zBb>`4=ga>T5=}sm$oYTzZKK1%pPKpalOx3-7Zd=qe~!o7JOYjdN?W^8>7NIos{Ua> z`1kfA)P*5*jDTY6h@W@KVr+E*5c&d-d7kan;R4afi5wvzG{OlUqEn-)e4VK z$fD(;EecTOu>`?mvj++}e#%eAC1PF~gxyUeSNN+B$pVXzbp?31%Lf%d2!9aVX*^Vf z6^e5Ob;>AN4g#w@zUA3oJvWu7v7y1~GGd7&r%>Tl11g3H7OztV$*KZe;vnwRbCdbv z=~3^gyL^rd1d`Z2R4#fYP8!W~@QA;nMj0b(E&wL~E1rGTm!9)e_w)UN_~7@=HiAV2 zhwo8_Ttb!3=MynqRBeX%s03v7)1 z4+2(TY$yleYwti}f&#pI*fV=JkyyM%(v0pmk6AG9q5_nQYU?@zL9K2;_nWU`H?Jvs z`vwWz09QL9e66bi-3sugi?hEAtN>|_1GHO!ei}I0!te?8Q6-J&Q~+iE26>>vG_Y$( zcO0No0h|_o7v1gXbSl7koZNLC99?J5lO}X3KrrND`2QHAh4_piX+p08=HQgf=GiH~+e0VwVCJuHcCg$nlVOvSGgfo&mnU zRLmLIDjt~R0`P)9n;P{Na8Xn)hoC8>(;06e`}03-ni}ybDEkNiD_r~vC@ui)k^z|F z0wDZV7JlWW^8Y%?W>fVLoGoSZ969_xE{!@n`RFsTOEBg>K@+Bz68`kqqrwNX6TT{$ z{=J>Or6J5d=li=CMm+@2_` z@ykZrs3jr}c>Z#yt>Tqer+d5dF0p7;(uB^b6ut3cf%*Y64at>rQ+d+eYZzO*k+UeX zGSBvT$@TX)TKLfNAUbbmM$W&lK0&xu6g>ui-B1Y@nm*~#pnDEt)?;-7lYf@;7Flh3 ztMzDDquIY1hx>p_K;Z}h;vn}GqGI{u&9<7euRVV>;U|YfYNQFBL11pL6U`sxvZ!uw z_#9{2u$SNl2Z?CAhVPU1YpH=5f0G6O>z#PdLQu}DEag%EH?U%@P!)-^91qiNw^rGl zSJPVB!J^z|O8{#yIg(R;dF({4Z)JecadHX&J+9=b#?H9$D=l+x!ZJ{|0w5Fi6-Uk0 zS7G9V?KCRK{loSQr%8L5oTWSC&WPbD1CDdV3|@nVwUF0{TeoSuz8xpABs$1L2iXg z1y$H;#5<4Lp*mfhS}OQBSR^S19ak~vO%en_5ClOG1VIo4K@bE%5QN_N58aSD9PZ?V QkN^Mx07*qoM6N<$g0z0fbN~PV literal 0 HcmV?d00001 diff --git a/assets/ChessAssets/b_King.png b/assets/ChessAssets/b_King.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7d13f8d8a833738d30e30bf016fd02e4e20284 GIT binary patch literal 2084 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 AxBvhE literal 0 HcmV?d00001 diff --git a/assets/ChessAssets/w_Bishop.png b/assets/ChessAssets/w_Bishop.png new file mode 100644 index 0000000000000000000000000000000000000000..473f5f4548e79db71069378bc39084f3b5993428 GIT binary patch literal 2747 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 LpI~M?Bc|{lqMX1v literal 0 HcmV?d00001 diff --git a/arabian-chess/license.txt b/assets/arabian-chess/license.txt similarity index 100% rename from arabian-chess/license.txt rename to assets/arabian-chess/license.txt diff --git a/arabian-chess/plane.png b/assets/arabian-chess/plane.png similarity index 100% rename from arabian-chess/plane.png rename to assets/arabian-chess/plane.png diff --git a/arabian-chess/player-one.gif b/assets/arabian-chess/player-one.gif similarity index 100% rename from arabian-chess/player-one.gif rename to assets/arabian-chess/player-one.gif diff --git a/arabian-chess/player-two.gif b/assets/arabian-chess/player-two.gif similarity index 100% rename from arabian-chess/player-two.gif rename to assets/arabian-chess/player-two.gif diff --git a/arabian-chess/raf.gif b/assets/arabian-chess/raf.gif similarity index 100% rename from arabian-chess/raf.gif rename to assets/arabian-chess/raf.gif diff --git a/arabian-chess/ref/cover.png b/assets/arabian-chess/ref/cover.png similarity index 100% rename from arabian-chess/ref/cover.png rename to assets/arabian-chess/ref/cover.png diff --git a/arabian-chess/ref/full_art.png b/assets/arabian-chess/ref/full_art.png similarity index 100% rename from arabian-chess/ref/full_art.png rename to assets/arabian-chess/ref/full_art.png diff --git a/arabian-chess/ref/logo.png b/assets/arabian-chess/ref/logo.png similarity index 100% rename from arabian-chess/ref/logo.png rename to assets/arabian-chess/ref/logo.png diff --git a/arabian-chess/sheets/board.png b/assets/arabian-chess/sheets/board.png similarity index 100% rename from arabian-chess/sheets/board.png rename to assets/arabian-chess/sheets/board.png diff --git a/arabian-chess/sheets/board_centered.png b/assets/arabian-chess/sheets/board_centered.png similarity index 100% rename from arabian-chess/sheets/board_centered.png rename to assets/arabian-chess/sheets/board_centered.png diff --git a/arabian-chess/sheets/board_without_bottom.png b/assets/arabian-chess/sheets/board_without_bottom.png similarity index 100% rename from arabian-chess/sheets/board_without_bottom.png rename to assets/arabian-chess/sheets/board_without_bottom.png diff --git a/arabian-chess/sheets/nums & letters.png b/assets/arabian-chess/sheets/nums & letters.png similarity index 100% rename from arabian-chess/sheets/nums & letters.png rename to assets/arabian-chess/sheets/nums & letters.png diff --git a/arabian-chess/sheets/pieces.png b/assets/arabian-chess/sheets/pieces.png similarity index 100% rename from arabian-chess/sheets/pieces.png rename to assets/arabian-chess/sheets/pieces.png diff --git a/arabian-chess/sprites/board/board_bottom.png b/assets/arabian-chess/sprites/board/board_bottom.png similarity index 100% rename from arabian-chess/sprites/board/board_bottom.png rename to assets/arabian-chess/sprites/board/board_bottom.png diff --git a/arabian-chess/sprites/board/board_square_black.png b/assets/arabian-chess/sprites/board/board_square_black.png similarity index 100% rename from arabian-chess/sprites/board/board_square_black.png rename to assets/arabian-chess/sprites/board/board_square_black.png diff --git a/arabian-chess/sprites/board/board_square_white.png b/assets/arabian-chess/sprites/board/board_square_white.png similarity index 100% rename from arabian-chess/sprites/board/board_square_white.png rename to assets/arabian-chess/sprites/board/board_square_white.png diff --git a/arabian-chess/sprites/nums & letters/letter_a.png b/assets/arabian-chess/sprites/nums & letters/letter_a.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_a.png rename to assets/arabian-chess/sprites/nums & letters/letter_a.png diff --git a/arabian-chess/sprites/nums & letters/letter_b.png b/assets/arabian-chess/sprites/nums & letters/letter_b.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_b.png rename to assets/arabian-chess/sprites/nums & letters/letter_b.png diff --git a/arabian-chess/sprites/nums & letters/letter_c.png b/assets/arabian-chess/sprites/nums & letters/letter_c.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_c.png rename to assets/arabian-chess/sprites/nums & letters/letter_c.png diff --git a/arabian-chess/sprites/nums & letters/letter_d.png b/assets/arabian-chess/sprites/nums & letters/letter_d.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_d.png rename to assets/arabian-chess/sprites/nums & letters/letter_d.png diff --git a/arabian-chess/sprites/nums & letters/letter_e.png b/assets/arabian-chess/sprites/nums & letters/letter_e.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_e.png rename to assets/arabian-chess/sprites/nums & letters/letter_e.png diff --git a/arabian-chess/sprites/nums & letters/letter_f.png b/assets/arabian-chess/sprites/nums & letters/letter_f.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_f.png rename to assets/arabian-chess/sprites/nums & letters/letter_f.png diff --git a/arabian-chess/sprites/nums & letters/letter_g.png b/assets/arabian-chess/sprites/nums & letters/letter_g.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_g.png rename to assets/arabian-chess/sprites/nums & letters/letter_g.png diff --git a/arabian-chess/sprites/nums & letters/letter_h.png b/assets/arabian-chess/sprites/nums & letters/letter_h.png similarity index 100% rename from arabian-chess/sprites/nums & letters/letter_h.png rename to assets/arabian-chess/sprites/nums & letters/letter_h.png diff --git a/arabian-chess/sprites/nums & letters/num_0.png b/assets/arabian-chess/sprites/nums & letters/num_0.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_0.png rename to assets/arabian-chess/sprites/nums & letters/num_0.png diff --git a/arabian-chess/sprites/nums & letters/num_1.png b/assets/arabian-chess/sprites/nums & letters/num_1.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_1.png rename to assets/arabian-chess/sprites/nums & letters/num_1.png diff --git a/arabian-chess/sprites/nums & letters/num_2.png b/assets/arabian-chess/sprites/nums & letters/num_2.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_2.png rename to assets/arabian-chess/sprites/nums & letters/num_2.png diff --git a/arabian-chess/sprites/nums & letters/num_3.png b/assets/arabian-chess/sprites/nums & letters/num_3.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_3.png rename to assets/arabian-chess/sprites/nums & letters/num_3.png diff --git a/arabian-chess/sprites/nums & letters/num_4.png b/assets/arabian-chess/sprites/nums & letters/num_4.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_4.png rename to assets/arabian-chess/sprites/nums & letters/num_4.png diff --git a/arabian-chess/sprites/nums & letters/num_5.png b/assets/arabian-chess/sprites/nums & letters/num_5.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_5.png rename to assets/arabian-chess/sprites/nums & letters/num_5.png diff --git a/arabian-chess/sprites/nums & letters/num_6.png b/assets/arabian-chess/sprites/nums & letters/num_6.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_6.png rename to assets/arabian-chess/sprites/nums & letters/num_6.png diff --git a/arabian-chess/sprites/nums & letters/num_7.png b/assets/arabian-chess/sprites/nums & letters/num_7.png similarity index 100% rename from arabian-chess/sprites/nums & letters/num_7.png rename to assets/arabian-chess/sprites/nums & letters/num_7.png diff --git a/arabian-chess/sprites/pieces/black_bishop.png b/assets/arabian-chess/sprites/pieces/black_bishop.png similarity index 100% rename from arabian-chess/sprites/pieces/black_bishop.png rename to assets/arabian-chess/sprites/pieces/black_bishop.png diff --git a/arabian-chess/sprites/pieces/black_king.png b/assets/arabian-chess/sprites/pieces/black_king.png similarity index 100% rename from arabian-chess/sprites/pieces/black_king.png rename to assets/arabian-chess/sprites/pieces/black_king.png diff --git a/arabian-chess/sprites/pieces/black_knight.png b/assets/arabian-chess/sprites/pieces/black_knight.png similarity index 100% rename from arabian-chess/sprites/pieces/black_knight.png rename to assets/arabian-chess/sprites/pieces/black_knight.png diff --git a/arabian-chess/sprites/pieces/black_pawn.png b/assets/arabian-chess/sprites/pieces/black_pawn.png similarity index 100% rename from arabian-chess/sprites/pieces/black_pawn.png rename to assets/arabian-chess/sprites/pieces/black_pawn.png diff --git a/arabian-chess/sprites/pieces/black_queen.png b/assets/arabian-chess/sprites/pieces/black_queen.png similarity index 100% rename from arabian-chess/sprites/pieces/black_queen.png rename to assets/arabian-chess/sprites/pieces/black_queen.png diff --git a/arabian-chess/sprites/pieces/black_rook.png b/assets/arabian-chess/sprites/pieces/black_rook.png similarity index 100% rename from arabian-chess/sprites/pieces/black_rook.png rename to assets/arabian-chess/sprites/pieces/black_rook.png diff --git a/arabian-chess/sprites/pieces/white_bishop.png b/assets/arabian-chess/sprites/pieces/white_bishop.png similarity index 100% rename from arabian-chess/sprites/pieces/white_bishop.png rename to assets/arabian-chess/sprites/pieces/white_bishop.png diff --git a/arabian-chess/sprites/pieces/white_king.png b/assets/arabian-chess/sprites/pieces/white_king.png similarity index 100% rename from arabian-chess/sprites/pieces/white_king.png rename to assets/arabian-chess/sprites/pieces/white_king.png diff --git a/arabian-chess/sprites/pieces/white_knight.png b/assets/arabian-chess/sprites/pieces/white_knight.png similarity index 100% rename from arabian-chess/sprites/pieces/white_knight.png rename to assets/arabian-chess/sprites/pieces/white_knight.png diff --git a/arabian-chess/sprites/pieces/white_pawn.png b/assets/arabian-chess/sprites/pieces/white_pawn.png similarity index 100% rename from arabian-chess/sprites/pieces/white_pawn.png rename to assets/arabian-chess/sprites/pieces/white_pawn.png diff --git a/arabian-chess/sprites/pieces/white_queen.png b/assets/arabian-chess/sprites/pieces/white_queen.png similarity index 100% rename from arabian-chess/sprites/pieces/white_queen.png rename to assets/arabian-chess/sprites/pieces/white_queen.png diff --git a/arabian-chess/sprites/pieces/white_rook.png b/assets/arabian-chess/sprites/pieces/white_rook.png similarity index 100% rename from arabian-chess/sprites/pieces/white_rook.png rename to assets/arabian-chess/sprites/pieces/white_rook.png diff --git a/arabian-chess/xav.png b/assets/arabian-chess/xav.png similarity index 100% rename from arabian-chess/xav.png rename to assets/arabian-chess/xav.png diff --git a/arabian-chess/zayne.png b/assets/arabian-chess/zayne.png similarity index 100% rename from arabian-chess/zayne.png rename to assets/arabian-chess/zayne.png diff --git a/package-lock.json b/package-lock.json index 323270f..6c981c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -458,7 +458,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz", "integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -475,7 +474,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz", "integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -489,7 +487,6 @@ "integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -522,7 +519,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz", "integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -566,7 +562,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz", "integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -633,7 +628,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1607,7 +1601,6 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3538,7 +3531,6 @@ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -3895,7 +3887,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4911,7 +4902,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5353,7 +5343,6 @@ "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -5850,8 +5839,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jose": { "version": "6.2.2", @@ -5953,7 +5941,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -6421,7 +6408,6 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -7934,7 +7920,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -7970,7 +7955,6 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -8590,8 +8574,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", @@ -8629,7 +8612,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8759,7 +8741,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9557,7 +9538,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -9576,8 +9556,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/proxy.conf.json b/proxy.conf.json index b1b7ec5..1f2ef6a 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -1,4 +1,14 @@ { + "/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, diff --git a/src/app/app.config.ts b/src/app/app.config.ts index b92a368..77e96f7 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,14 +1,15 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; +import { authInterceptor } from './services/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true }), - provideHttpClient(), + provideHttpClient(withInterceptors([authInterceptor])), provideRouter(routes) ] }; diff --git a/src/app/app.html b/src/app/app.html index 67e7bd4..515b2b7 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1 +1,2 @@ - + + \ No newline at end of file diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 215ff45..9fb0ff3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,9 +1,11 @@ import { Routes } from '@angular/router'; import { GameComponent } from './pages/game/game.component'; import { WelcomeComponent } from './pages/welcome/welcome.component'; +import { ProfileComponent } from './pages/profile/profile.component'; export const routes: Routes = [ { path: '', component: WelcomeComponent }, + { path: 'profile', component: ProfileComponent }, { path: 'game/:gameId', component: GameComponent }, { path: '**', redirectTo: '' } ]; diff --git a/src/app/app.ts b/src/app/app.ts index 6aba061..4206e4c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,21 +1,18 @@ import { Component, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { ToolbarComponent } from './components/toolbar/toolbar.component'; +import { ThemeService } from './services/theme.service'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, ToolbarComponent], templateUrl: './app.html', styleUrl: './app.css' }) export class App implements OnInit { - ngOnInit(): void { - this.initTheme(); - } + constructor(private readonly themeService: ThemeService) { } - private initTheme(): void { - const savedTheme = localStorage.getItem('theme'); - if (savedTheme === 'dark') { - document.documentElement.setAttribute('data-theme', 'dark'); - } + ngOnInit(): void { + this.themeService.initTheme(); } } diff --git a/src/app/button-template.css b/src/app/button-template.css new file mode 100644 index 0000000..5b8888a --- /dev/null +++ b/src/app/button-template.css @@ -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; +} diff --git a/src/app/components/chess-board/chess-board.component.css b/src/app/components/chess-board/chess-board.component.css index a86b1b1..47b3421 100644 --- a/src/app/components/chess-board/chess-board.component.css +++ b/src/app/components/chess-board/chess-board.component.css @@ -27,12 +27,56 @@ cursor: pointer; } +.square[draggable='true'] { + cursor: grab; +} + +.square.drag-source { + opacity: 0.65; + cursor: grabbing; +} + +.square.drag-over { + outline: 3px dashed var(--color-primary); + outline-offset: -4px; +} + .square.light { - background-image: url('/arabian-chess/sprites/board/board_square_white.png'); + background-image: url('/assets/arabian-chess/sprites/board/board_square_white.png'); } .square.dark { - background-image: url('/arabian-chess/sprites/board/board_square_black.png'); + background-image: url('/assets/arabian-chess/sprites/board/board_square_black.png'); +} + +.board-grid--classic { + border-radius: var(--border-radius-md); +} + +.board-grid--classic .square { + background-image: none; + transition: filter 160ms ease; +} + +.board-grid--classic .square.light { + background-color: #f3c8a0; +} + +.board-grid--classic .square.dark { + background-color: #ba6d4b; +} + +.board-grid--classic .square.drag-over { + outline-color: #5a2c28; +} + +.board-grid--classic .square.selected { + outline-color: #5a2c28; +} + +.board-grid--classic .square.highlighted::after { + background: #b9dad1; + border-color: #5a2c28; } .square.highlighted::after { diff --git a/src/app/components/chess-board/chess-board.component.html b/src/app/components/chess-board/chess-board.component.html index 7bc8e4a..0704d39 100644 --- a/src/app/components/chess-board/chess-board.component.html +++ b/src/app/components/chess-board/chess-board.component.html @@ -1,5 +1,5 @@
-
+
@for (square of squares; track trackByCoordinate($index, square)) { }
- Board frame + @if (boardTheme === 'arabian') { + Board frame + }
diff --git a/src/app/components/chess-board/chess-board.component.ts b/src/app/components/chess-board/chess-board.component.ts index 179ad63..0a1a1a8 100644 --- a/src/app/components/chess-board/chess-board.component.ts +++ b/src/app/components/chess-board/chess-board.component.ts @@ -7,6 +7,8 @@ interface BoardSquare { pieceCode: string | null; } +type BoardTheme = 'arabian' | 'classic'; + @Component({ selector: 'app-chess-board', standalone: true, @@ -18,10 +20,14 @@ export class ChessBoardComponent implements OnChanges { @Input({ required: true }) fen = ''; @Input() selectedSquare: string | null = null; @Input() highlightedSquares: string[] = []; + @Input() boardTheme: BoardTheme = 'arabian'; @Output() squareSelected = new EventEmitter(); squares: BoardSquare[] = []; private highlightedSquareSet = new Set(); + private draggingFromSquare: string | null = null; + private dragOverSquare: string | null = null; + private suppressNextClick = false; ngOnChanges(changes: SimpleChanges): void { if (changes['fen']) { @@ -38,9 +44,61 @@ export class ChessBoardComponent implements OnChanges { } onSquareClick(square: BoardSquare): void { + if (this.suppressNextClick) { + this.suppressNextClick = false; + return; + } + this.squareSelected.emit(square.coordinate); } + onPieceDragStart(event: DragEvent, square: BoardSquare): void { + if (!square.pieceCode) { + event.preventDefault(); + return; + } + + this.draggingFromSquare = square.coordinate; + if (event.dataTransfer) { + event.dataTransfer.setData('text/plain', square.coordinate); + event.dataTransfer.effectAllowed = 'move'; + } + this.squareSelected.emit(square.coordinate); + } + + onSquareDragOver(event: DragEvent, square: BoardSquare): void { + if (!this.draggingFromSquare) { + return; + } + + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + this.dragOverSquare = square.coordinate === this.draggingFromSquare ? null : square.coordinate; + } + + onSquareDrop(event: DragEvent, square: BoardSquare): void { + event.preventDefault(); + if (!this.draggingFromSquare) { + return; + } + + const fromSquare = this.draggingFromSquare; + this.clearDragState(); + + if (fromSquare === square.coordinate) { + return; + } + + this.suppressNextClick = true; + this.squareSelected.emit(square.coordinate); + } + + onSquareDragEnd(): void { + this.clearDragState(); + } + isSelected(square: BoardSquare): boolean { return this.selectedSquare === square.coordinate; } @@ -49,6 +107,14 @@ export class ChessBoardComponent implements OnChanges { return this.highlightedSquareSet.has(square.coordinate); } + isDraggingSource(square: BoardSquare): boolean { + return this.draggingFromSquare === square.coordinate; + } + + isDragOver(square: BoardSquare): boolean { + return this.dragOverSquare === square.coordinate; + } + private buildSquares(fen: string): BoardSquare[] { const placement = fen.split(' ')[0] ?? ''; const rows = placement.split('/'); @@ -87,4 +153,9 @@ export class ChessBoardComponent implements OnChanges { pieceCode }; } + + private clearDragState(): void { + this.draggingFromSquare = null; + this.dragOverSquare = null; + } } diff --git a/src/app/components/chess-piece/chess-piece.component.css b/src/app/components/chess-piece/chess-piece.component.css index 4a364f3..f876000 100644 --- a/src/app/components/chess-piece/chess-piece.component.css +++ b/src/app/components/chess-piece/chess-piece.component.css @@ -3,7 +3,15 @@ height: clamp(40px, 8cqh, 120px); display: block; object-fit: contain; - pointer-events: none; + pointer-events: auto; +} + +.piece[draggable='true'] { + cursor: grab; +} + +.piece[draggable='true']:active { + cursor: grabbing; } @media (max-width: 991px) { diff --git a/src/app/components/chess-piece/chess-piece.component.html b/src/app/components/chess-piece/chess-piece.component.html index 42766a1..7d085f4 100644 --- a/src/app/components/chess-piece/chess-piece.component.html +++ b/src/app/components/chess-piece/chess-piece.component.html @@ -1,3 +1,10 @@ @if (pieceCode) { - + } diff --git a/src/app/components/chess-piece/chess-piece.component.ts b/src/app/components/chess-piece/chess-piece.component.ts index 2804978..6b3e125 100644 --- a/src/app/components/chess-piece/chess-piece.component.ts +++ b/src/app/components/chess-piece/chess-piece.component.ts @@ -1,4 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +type BoardTheme = 'arabian' | 'classic'; @Component({ selector: 'app-chess-piece', @@ -8,18 +10,44 @@ import { Component, Input } from '@angular/core'; }) export class ChessPieceComponent { @Input({ required: true }) pieceCode: string | null = null; + @Input() boardTheme: BoardTheme = 'arabian'; + @Input() draggable = false; + @Output() pieceDragStart = new EventEmitter(); + @Output() pieceDragEnd = new EventEmitter(); + + onDragStart(event: DragEvent): void { + if (!this.draggable) { + event.preventDefault(); + return; + } + + this.pieceDragStart.emit(event); + } + + onDragEnd(): void { + this.pieceDragEnd.emit(); + } get spriteUrl(): string { if (!this.pieceCode) { return ''; } - const color = this.pieceCode === this.pieceCode.toUpperCase() ? 'white' : 'black'; - const pieceName = this.getPieceName(this.pieceCode.toLowerCase()); - return `/arabian-chess/sprites/pieces/${color}_${pieceName}.png`; + const isWhite = this.pieceCode === this.pieceCode.toUpperCase(); + const pieceCode = this.pieceCode.toLowerCase(); + + if (this.boardTheme === 'classic') { + const colorPrefix = isWhite ? 'w' : 'b'; + const classicPieceName = this.getClassicPieceName(pieceCode); + return `/assets/ChessAssets/${colorPrefix}_${classicPieceName}.png`; + } + + const arabianColor = isWhite ? 'white' : 'black'; + const arabianPieceName = this.getArabianPieceName(pieceCode); + return `/assets/arabian-chess/sprites/pieces/${arabianColor}_${arabianPieceName}.png`; } - private getPieceName(piece: string): string { + private getArabianPieceName(piece: string): string { switch (piece) { case 'k': return 'king'; @@ -37,4 +65,23 @@ export class ChessPieceComponent { return 'pawn'; } } + + private getClassicPieceName(piece: string): string { + switch (piece) { + case 'k': + return 'King'; + case 'q': + return 'Queen'; + case 'r': + return 'Rook'; + case 'b': + return 'Bishop'; + case 'n': + return 'Knight'; + case 'p': + return 'Pawn'; + default: + return 'Pawn'; + } + } } diff --git a/src/app/components/input-card/input-card.component.css b/src/app/components/input-card/input-card.component.css index b4cb7a4..65aa86b 100644 --- a/src/app/components/input-card/input-card.component.css +++ b/src/app/components/input-card/input-card.component.css @@ -1,3 +1,5 @@ +@import '../../button-template.css'; + .input-card { background: var(--color-bg-card); border: var(--border-width) solid var(--color-border); @@ -40,22 +42,6 @@ } -.input-card .btn { - border: var(--button-border); - border-radius: var(--border-radius-sm); - background: var(--color-bg-button); - color: var(--color-text-primary); - padding: var(--button-padding); - cursor: pointer; - font-weight: 600; - transition: background-color 0.2s, color 0.2s; -} - -.input-card .btn:hover { - background: var(--color-bg-button-hover); - color: var(--color-text-button-hover); -} - .hint-text { margin: 0; color: var(--color-text-primary); diff --git a/src/app/components/input-card/input-card.component.html b/src/app/components/input-card/input-card.component.html index 34b9d1a..bdc6202 100644 --- a/src/app/components/input-card/input-card.component.html +++ b/src/app/components/input-card/input-card.component.html @@ -2,30 +2,18 @@ @if (inputType === 'textarea') { - + } @else { - + } - @if (hintText) { -

{{ hintText }}

+

{{ hintText }}

} - + \ No newline at end of file diff --git a/src/app/components/login-dialog/login-dialog.component.css b/src/app/components/login-dialog/login-dialog.component.css new file mode 100644 index 0000000..acea1e8 --- /dev/null +++ b/src/app/components/login-dialog/login-dialog.component.css @@ -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; +} diff --git a/src/app/components/login-dialog/login-dialog.component.html b/src/app/components/login-dialog/login-dialog.component.html new file mode 100644 index 0000000..0f3d9e4 --- /dev/null +++ b/src/app/components/login-dialog/login-dialog.component.html @@ -0,0 +1,36 @@ +
+
+
LOGIN
+ +
+ + + @if (loginForm.get('username')?.invalid && loginForm.get('username')?.touched) { + Username must be at least 3 characters + } + + + + @if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) { + Password must be at least 6 characters + } + + @if (errorMessage) { +
{{ errorMessage }}
+ } + +
+ + + +
+
+
+
\ No newline at end of file diff --git a/src/app/components/login-dialog/login-dialog.component.ts b/src/app/components/login-dialog/login-dialog.component.ts new file mode 100644 index 0000000..25a74a2 --- /dev/null +++ b/src/app/components/login-dialog/login-dialog.component.ts @@ -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(); + @Output() onSuccess = new EventEmitter(); + + 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(); + } +} diff --git a/src/app/components/promotion-dialog/promotion-dialog.component.css b/src/app/components/promotion-dialog/promotion-dialog.component.css index 578f349..aedff18 100644 --- a/src/app/components/promotion-dialog/promotion-dialog.component.css +++ b/src/app/components/promotion-dialog/promotion-dialog.component.css @@ -1,3 +1,5 @@ +@import '../../button-template.css'; + .promotion-dialog-overlay { position: fixed; top: 0; @@ -20,9 +22,10 @@ } .promotion-dialog { - background: white; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + 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; @@ -44,32 +47,14 @@ justify-content: space-between; align-items: center; padding: 20px; - border-bottom: 1px solid #e0e0e0; + border-bottom: 1px solid var(--bb-border, #e0e0e0); h3 { margin: 0; + font-family: 'Bebas Neue', sans-serif; font-size: 18px; font-weight: 600; - color: #333; - } - - .close-btn { - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: #999; - padding: 0; - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s; - - &:hover { - color: #333; - } + color: var(--bb-title, #333); } } @@ -80,7 +65,8 @@ .promotion-prompt { margin: 0 0 20px 0; text-align: center; - color: #666; + color: var(--bb-title); + opacity: 0.8; font-size: 14px; } @@ -89,43 +75,3 @@ grid-template-columns: 1fr 1fr; gap: 12px; } - -.promotion-button { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; - padding: 16px; - background: #f5f5f5; - border: 2px solid #ddd; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; - font-family: inherit; - - &:hover { - background: #e8e8e8; - border-color: #999; - transform: translateY(-2px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } - - &:active { - transform: translateY(0); - box-shadow: none; - } - - .piece-symbol { - font-size: 32px; - line-height: 1; - } - - .piece-label { - font-size: 12px; - font-weight: 500; - color: #666; - text-transform: uppercase; - letter-spacing: 0.5px; - } -} diff --git a/src/app/components/promotion-dialog/promotion-dialog.component.html b/src/app/components/promotion-dialog/promotion-dialog.component.html index 7cdb899..420e0bc 100644 --- a/src/app/components/promotion-dialog/promotion-dialog.component.html +++ b/src/app/components/promotion-dialog/promotion-dialog.component.html @@ -2,25 +2,22 @@

Pawn Promotion

- +
- +

Choose a piece to promote your pawn to:

- +
@for (piece of promotionPieces; track piece.type) { - + }
-
+ \ No newline at end of file diff --git a/src/app/components/register-dialog/register-dialog.component.css b/src/app/components/register-dialog/register-dialog.component.css new file mode 100644 index 0000000..acea1e8 --- /dev/null +++ b/src/app/components/register-dialog/register-dialog.component.css @@ -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; +} diff --git a/src/app/components/register-dialog/register-dialog.component.html b/src/app/components/register-dialog/register-dialog.component.html new file mode 100644 index 0000000..6562df8 --- /dev/null +++ b/src/app/components/register-dialog/register-dialog.component.html @@ -0,0 +1,43 @@ +
+
+
CREATE ACCOUNT
+ +
+ + @if (registerForm.get('username')?.invalid && registerForm.get('username')?.touched) { + Username must be at least 3 characters + } + + + @if (registerForm.get('email')?.invalid && registerForm.get('email')?.touched) { + Please enter a valid email + } + + + @if (registerForm.get('password')?.invalid && registerForm.get('password')?.touched) { + Password must be at least 6 characters + } + + + + @if (errorMessage) { +
{{ errorMessage }}
+ } + +
+ + + +
+
+
+
\ No newline at end of file diff --git a/src/app/components/register-dialog/register-dialog.component.ts b/src/app/components/register-dialog/register-dialog.component.ts new file mode 100644 index 0000000..ef11842 --- /dev/null +++ b/src/app/components/register-dialog/register-dialog.component.ts @@ -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(); + @Output() onSuccess = new EventEmitter(); + + 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(); + } +} diff --git a/src/app/components/toolbar/toolbar.component.css b/src/app/components/toolbar/toolbar.component.css new file mode 100644 index 0000000..be8f759 --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.css @@ -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; +} diff --git a/src/app/components/toolbar/toolbar.component.html b/src/app/components/toolbar/toolbar.component.html new file mode 100644 index 0000000..cba57a3 --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.html @@ -0,0 +1,35 @@ + + +@if (showLoginDialog) { + +} + +@if (showRegisterDialog) { + +} \ No newline at end of file diff --git a/src/app/components/toolbar/toolbar.component.ts b/src/app/components/toolbar/toolbar.component.ts new file mode 100644 index 0000000..bc4a815 --- /dev/null +++ b/src/app/components/toolbar/toolbar.component.ts @@ -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(); + } +} diff --git a/src/app/models/auth.models.ts b/src/app/models/auth.models.ts new file mode 100644 index 0000000..0af910d --- /dev/null +++ b/src/app/models/auth.models.ts @@ -0,0 +1,28 @@ +export interface LoginRequest { + username: string; + password: string; +} + +export interface RegisterRequest { + username: string; + password: string; + email?: string; +} + +export interface RegisterResponse { + id: string; + username: string; + rating: number; + createdAt: string; +} + +export interface LoginResponse { + token: string; +} + +export interface CurrentUser { + id: string; + username: string; + rating: number; + createdAt: string; +} diff --git a/src/app/pages/game/game.component.css b/src/app/pages/game/game.component.css index 042bcef..130e475 100644 --- a/src/app/pages/game/game.component.css +++ b/src/app/pages/game/game.component.css @@ -1,6 +1,16 @@ +@import '../../button-template.css'; + .game-shell { min-height: 100dvh; padding: clamp(var(--size-md), 2vw, var(--size-xl)); + background: linear-gradient(180deg, var(--color-primary-light) 0%, var(--color-secondary-mint) 100%); + color: var(--color-text-primary); +} + +:host-context(html[data-theme='dark']) .game-shell { + background: + radial-gradient(circle at top, rgba(185, 194, 218, 0.16) 0%, transparent 35%), + linear-gradient(180deg, #0f1f2e 0%, #17293d 52%, #0b1420 100%); } .game-card { @@ -13,8 +23,14 @@ box-shadow: var(--shadow-md); } +:host-context(html[data-theme='dark']) .game-shell .game-card { + background: rgba(26, 47, 71, 0.88); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.34); +} + header { margin-bottom: var(--size-xl); + margin-bottom: var(--size-xl); } h1, @@ -22,9 +38,13 @@ h2 { color: var(--color-text-primary); margin: 0 0 var(--size-md); font-size: var(--heading-h1); + color: var(--color-text-primary); + margin: 0 0 var(--size-md); + font-size: var(--heading-h1); } .meta { + color: var(--color-text-primary); color: var(--color-text-primary); font-size: 0.95rem; } @@ -33,6 +53,8 @@ h2 { display: inline-block; margin-bottom: var(--size-sm); color: var(--color-text-primary); + margin-bottom: var(--size-sm); + color: var(--color-text-primary); text-decoration: none; font-weight: 600; } @@ -48,6 +70,39 @@ h2 { flex: 0 0 auto; } +.board-theme-card { + background: var(--color-bg-card); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-md-padding); + display: grid; + gap: var(--size-sm); +} + +.board-theme-card h3 { + margin: 0; + color: var(--color-text-primary); + font-size: 1rem; +} + +.board-theme-group { + display: flex; + gap: var(--size-md); + flex-wrap: wrap; +} + +.board-theme-option { + display: inline-flex; + align-items: center; + gap: var(--size-xs); + color: var(--color-text-primary); + font-weight: 600; +} + +.board-theme-option input { + accent-color: var(--color-primary); +} + .move-card { padding: var(--size-lg-padding); } @@ -62,6 +117,10 @@ h2 { } .board-section { + background: var(--color-bg-board); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: clamp(var(--size-sm), 1vw, var(--size-lg)); background: var(--color-bg-board); border: var(--border-width) solid var(--color-border); border-radius: var(--border-radius-md); @@ -70,6 +129,148 @@ h2 { container-type: size; } +:host-context(html[data-theme='dark']) .game-shell .board-section, +:host-context(html[data-theme='dark']) .game-shell .timer-card, +:host-context(html[data-theme='dark']) .game-shell .history-card, +:host-context(html[data-theme='dark']) .game-shell .export-card, +:host-context(html[data-theme='dark']) .game-shell .board-theme-card, +:host-context(html[data-theme='dark']) .game-shell .player-timer { + background: rgba(45, 74, 111, 0.72); +} + +:host-context(html[data-theme='dark']) .game-shell .export-text { + background: rgba(26, 47, 71, 0.9); +} + +:host-context(html[data-theme='dark']) .game-shell .game-completion-alert { + background: linear-gradient(135deg, rgba(74, 124, 124, 0.35) 0%, rgba(90, 111, 165, 0.35) 100%); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25); +} + +.timer-card, +.history-card, +.export-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: grid; + gap: var(--size-md); +} + +.timer-card h2, +.history-card h2, +.export-card h2 { + margin: 0; + font-size: 1.1rem; + color: var(--color-text-primary); +} + +.history-list { + margin: 0; + padding-left: 1.1rem; + display: grid; + gap: var(--size-xs); + max-height: 180px; + overflow: auto; +} + +.history-list li { + color: var(--color-text-primary); + display: flex; + gap: var(--size-sm); + align-items: baseline; +} + +.history-number { + font-weight: 700; + min-width: 1.8rem; +} + +.history-move { + font-family: monospace; +} + +.history-empty { + margin: 0; + color: var(--color-text-primary); +} + +.player-timer { + background: var(--color-bg-input); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-md-padding); +} + +.active-timer { + box-shadow: 0 0 0 3px rgba(185, 218, 209, 0.25); +} + +.timer-label { + margin: 0; + color: var(--color-text-primary); + font-weight: 600; +} + +.timer-value { + margin: var(--size-xs) 0 0; + color: var(--color-text-primary); + font-size: 1.35rem; + font-weight: 700; +} + +.export-mode-group { + display: flex; + gap: var(--size-lg); + flex-wrap: wrap; +} + +.export-mode-option { + display: inline-flex; + align-items: center; + gap: var(--size-sm); + color: var(--color-text-primary); + font-weight: 600; +} + +.export-mode-option input { + accent-color: var(--color-primary); +} + +.export-text { + width: 100%; + min-height: 140px; + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + background: var(--color-bg-input); + color: var(--color-text-primary); + padding: var(--size-md-padding); + resize: vertical; +} + +.export-button { + width: fit-content; + border: var(--button-border); + border-radius: var(--button-radius); + background: var(--color-bg-button); + color: var(--color-text-primary); + font-weight: 700; + padding: var(--button-padding); + cursor: pointer; +} + +.export-button:hover { + background: var(--color-bg-button-hover); + color: var(--color-text-button-hover); +} + +.export-note { + margin: 0; + color: var(--color-text-primary); + font-weight: 600; +} + .alert { border-radius: var(--border-radius-sm); border: var(--border-width) solid var(--color-border); @@ -126,6 +327,7 @@ h2 { @media (max-width: 991px) { .game-card { padding: clamp(var(--size-md), 1.5vw, var(--size-lg)); + padding: clamp(var(--size-md), 1.5vw, var(--size-lg)); } .board-section { @@ -135,25 +337,30 @@ h2 { h1, h2 { font-size: var(--heading-h1-tablet); + font-size: var(--heading-h1-tablet); } } @media (max-width: 768px) { .game-shell { padding: clamp(var(--size-sm), 1.5vw, var(--size-lg)); + padding: clamp(var(--size-sm), 1.5vw, var(--size-lg)); } .game-card { padding: clamp(var(--size-sm), 1vw, var(--size-md)); + padding: clamp(var(--size-sm), 1vw, var(--size-md)); } header { margin-bottom: var(--size-lg); + margin-bottom: var(--size-lg); } h1, h2 { font-size: var(--heading-h1-mobile); + font-size: var(--heading-h1-mobile); } .meta { @@ -163,6 +370,8 @@ h2 { .top-section { gap: var(--size-xs); margin-bottom: var(--size-xs); + gap: var(--size-xs); + margin-bottom: var(--size-xs); } .board-section { @@ -173,19 +382,24 @@ h2 { @media (max-width: 480px) { .game-shell { padding: var(--size-sm); + padding: var(--size-sm); } .game-card { padding: var(--size-sm); border-radius: var(--border-radius-md); + padding: var(--size-sm); + border-radius: var(--border-radius-md); } header { margin-bottom: var(--size-md); + margin-bottom: var(--size-md); } h1 { font-size: var(--heading-h1-small); + font-size: var(--heading-h1-small); } .meta { @@ -195,6 +409,8 @@ h2 { .top-section { gap: var(--size-xs-gap); margin-bottom: var(--size-xs); + gap: var(--size-xs-gap); + margin-bottom: var(--size-xs); } .board-section { diff --git a/src/app/pages/game/game.component.html b/src/app/pages/game/game.component.html index 04c93b2..dac651b 100644 --- a/src/app/pages/game/game.component.html +++ b/src/app/pages/game/game.component.html @@ -1,9 +1,6 @@ -
- +
+
@@ -13,79 +10,126 @@
@if (facade.loading) { -

Loading game state...

+

Loading game state...

} @else if (facade.state) { - @if (facade.isGameFinished && facade.gameCompletionMessage) { -
-

{{ facade.gameCompletionMessage }}

-

- Start a new game -

+ @if (facade.isGameFinished && facade.gameCompletionMessage) { +
+

{{ facade.gameCompletionMessage }}

+

+ Start a new game +

+
+ } + @if (facade.isGameFinished && facade.gameCompletionMessage) { +
+

{{ facade.gameCompletionMessage }}

+

+ Start a new game +

+
+ } +
+
+ +
+
+

Timers

+
+

White

+

{{ formatTimer(whiteTimerSeconds) }}

+
+
+

Black

+

{{ formatTimer(blackTimerSeconds) }}

+
+
- } -
-
- -
- -
- -
-
-
- -
+ +
+
+
+ +
-
- +
+
+

Board Design

+
+ + + +
-
-
- -
- -
+ +
+
+ + + +
+
+

Move History

+ + @if (facade.state.moves.length === 0) { +

No moves yet.

+ } @else { +
    + @for (move of facade.state.moves; track $index) { +
  1. + {{ $index + 1 }}. + {{ move }} +
  2. + } +
+ } +
+ +
+

Export

+
+ + +
+ + + + + + @if (exportNotice) { +

{{ exportNotice }}

+ } +
+ } @if (facade.errorMessage) { -

{{ facade.errorMessage }}

+

{{ facade.errorMessage }}

} -
+
\ No newline at end of file diff --git a/src/app/pages/game/game.component.ts b/src/app/pages/game/game.component.ts index 307aed4..a7c14fb 100644 --- a/src/app/pages/game/game.component.ts +++ b/src/app/pages/game/game.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, DestroyRef, OnInit, inject } from '@angular/core'; +import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; @@ -8,6 +8,16 @@ import { InputCardComponent } from '../../components/input-card/input-card.compo import { PromotionDialogComponent } from '../../components/promotion-dialog/promotion-dialog.component'; import { GameFacade } from './game.facade'; +type TimerTurn = 'white' | 'black'; +type BoardTheme = 'arabian' | 'classic'; + +interface TimerSnapshot { + whiteSeconds: number; + blackSeconds: number; + turn: TimerTurn; + savedAt: number; +} + @Component({ selector: 'app-game', standalone: true, @@ -16,12 +26,28 @@ import { GameFacade } from './game.facade'; templateUrl: './game.component.html', styleUrl: './game.component.css' }) -export class GameComponent implements OnInit { +export class GameComponent implements OnInit, OnDestroy { + private static readonly TIMER_START_SECONDS = 10 * 60; + private static readonly BOARD_THEME_STORAGE_KEY = 'nowchess.boardTheme'; private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); readonly facade = inject(GameFacade); + whiteTimerSeconds = GameComponent.TIMER_START_SECONDS; + blackTimerSeconds = GameComponent.TIMER_START_SECONDS; + exportType: 'fen' | 'pgn' = 'fen'; + boardTheme: BoardTheme = 'arabian'; + isDarkMode = false; + exportValue = ''; + exportNotice = ''; + private timerIntervalId: number | null = null; + private activeGameId = ''; ngOnInit(): void { + this.applyIncomingTheme(); + this.syncThemeFromDocument(); + this.boardTheme = this.resolveStoredBoardTheme(); + this.startDummyTimers(); + this.route.paramMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((paramMap) => { const id = paramMap.get('gameId'); if (!id) { @@ -30,7 +56,203 @@ export class GameComponent implements OnInit { return; } + this.activeGameId = id; + this.restoreTimers(id); this.facade.setGameId(id); + this.syncExportValue(); }); } + + ngOnDestroy(): void { + if (this.timerIntervalId !== null) { + window.clearInterval(this.timerIntervalId); + } + + this.persistTimers(this.resolveCurrentTurn()); + } + + private syncThemeFromDocument(): void { + this.isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark'; + } + + private applyIncomingTheme(): void { + const incomingTheme = window.history.state?.theme; + if (incomingTheme === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + localStorage.setItem('theme', 'dark'); + return; + } + + if (incomingTheme === 'light') { + document.documentElement.removeAttribute('data-theme'); + localStorage.removeItem('theme'); + } + } + + setExportType(type: 'fen' | 'pgn'): void { + this.exportType = type; + this.exportNotice = ''; + this.syncExportValue(); + } + + setBoardTheme(theme: BoardTheme): void { + this.boardTheme = theme; + localStorage.setItem(GameComponent.BOARD_THEME_STORAGE_KEY, theme); + } + + completeExport(): void { + this.syncExportValue(); + if (!this.exportValue.trim()) { + this.exportNotice = 'Nothing to export yet.'; + return; + } + + if (!navigator.clipboard?.writeText) { + this.exportNotice = 'Export is ready in the text box.'; + return; + } + + void navigator.clipboard + .writeText(this.exportValue) + .then(() => { + this.exportNotice = 'Copied to clipboard.'; + }) + .catch(() => { + this.exportNotice = 'Export is ready in the text box.'; + }); + } + + formatTimer(totalSeconds: number): string { + const safeSeconds = Math.max(0, totalSeconds); + const minutes = Math.floor(safeSeconds / 60) + .toString() + .padStart(2, '0'); + const seconds = (safeSeconds % 60).toString().padStart(2, '0'); + return `${minutes}:${seconds}`; + } + + private startDummyTimers(): void { + if (this.timerIntervalId !== null) { + return; + } + + this.timerIntervalId = window.setInterval(() => { + this.tickDummyTimers(); + this.syncExportValue(); + }, 1000); + } + + private tickDummyTimers(): void { + const state = this.facade.state; + if (!state || this.facade.loading || this.facade.isGameFinished) { + return; + } + + if (state.turn === 'white') { + this.whiteTimerSeconds = Math.max(0, this.whiteTimerSeconds - 1); + this.persistTimers('white'); + return; + } + + this.blackTimerSeconds = Math.max(0, this.blackTimerSeconds - 1); + this.persistTimers('black'); + } + + private syncExportValue(): void { + const state = this.facade.state; + if (!state) { + this.exportValue = ''; + return; + } + + this.exportValue = this.exportType === 'fen' ? state.fen : state.pgn; + } + + private restoreTimers(gameId: string): void { + const fallbackTurn = this.resolveCurrentTurn(); + const rawSnapshot = localStorage.getItem(this.getTimerStorageKey(gameId)); + if (!rawSnapshot) { + this.resetTimers(); + this.persistTimers(fallbackTurn); + return; + } + + const snapshot = this.parseSnapshot(rawSnapshot); + if (!snapshot) { + this.resetTimers(); + this.persistTimers(fallbackTurn); + return; + } + + this.applySnapshot(snapshot); + this.persistTimers(snapshot.turn); + } + + private parseSnapshot(rawSnapshot: string): TimerSnapshot | null { + try { + const parsed = JSON.parse(rawSnapshot) as Partial; + if ( + typeof parsed.whiteSeconds !== 'number' || + typeof parsed.blackSeconds !== 'number' || + (parsed.turn !== 'white' && parsed.turn !== 'black') || + typeof parsed.savedAt !== 'number' + ) { + return null; + } + + return { + whiteSeconds: Math.max(0, Math.floor(parsed.whiteSeconds)), + blackSeconds: Math.max(0, Math.floor(parsed.blackSeconds)), + turn: parsed.turn, + savedAt: parsed.savedAt + }; + } catch { + return null; + } + } + + private applySnapshot(snapshot: TimerSnapshot): void { + const elapsedSeconds = Math.max(0, Math.floor((Date.now() - snapshot.savedAt) / 1000)); + this.whiteTimerSeconds = snapshot.whiteSeconds; + this.blackTimerSeconds = snapshot.blackSeconds; + + if (snapshot.turn === 'white') { + this.whiteTimerSeconds = Math.max(0, this.whiteTimerSeconds - elapsedSeconds); + return; + } + + this.blackTimerSeconds = Math.max(0, this.blackTimerSeconds - elapsedSeconds); + } + + private persistTimers(turn: TimerTurn): void { + if (!this.activeGameId) { + return; + } + + const snapshot: TimerSnapshot = { + whiteSeconds: this.whiteTimerSeconds, + blackSeconds: this.blackTimerSeconds, + turn, + savedAt: Date.now() + }; + localStorage.setItem(this.getTimerStorageKey(this.activeGameId), JSON.stringify(snapshot)); + } + + private resolveCurrentTurn(): TimerTurn { + return this.facade.state?.turn ?? 'white'; + } + + private resetTimers(): void { + this.whiteTimerSeconds = GameComponent.TIMER_START_SECONDS; + this.blackTimerSeconds = GameComponent.TIMER_START_SECONDS; + } + + private getTimerStorageKey(gameId: string): string { + return `nowchess.timer.${gameId}`; + } + + private resolveStoredBoardTheme(): BoardTheme { + const stored = localStorage.getItem(GameComponent.BOARD_THEME_STORAGE_KEY); + return stored === 'classic' ? 'classic' : 'arabian'; + } } diff --git a/src/app/pages/profile/profile.component.css b/src/app/pages/profile/profile.component.css new file mode 100644 index 0000000..595c49e --- /dev/null +++ b/src/app/pages/profile/profile.component.css @@ -0,0 +1,478 @@ +@import '../welcome/welcome.component.css'; + +.profile-building-container { + position: absolute; + bottom: 10%; + left: 50%; + transform: translateX(-50%); + z-index: 15; +} + +.building-wrapper { + position: relative; + width: clamp(300px, 80vw, 600px); + margin: 0 auto; +} + +.building-structure { + background: linear-gradient(135deg, var(--bldg-body) 0%, var(--bldg-mid) 50%, var(--bldg-lit) 100%); + border: 2px solid var(--dlg-border); + border-radius: 8px; + box-shadow: var(--bb-glow), inset 0 0 20px rgba(0, 210, 255, 0.1); + overflow: hidden; +} + +.cityscape-shell.sunset .building-structure { + box-shadow: var(--bb-glow), inset 0 0 20px rgba(255, 120, 40, 0.1); +} + +.building-top { + height: 30px; + background: linear-gradient(180deg, var(--bldg-mid) 0%, var(--bldg-body) 100%); + border-bottom: 1px solid rgba(0, 210, 255, 0.3); + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: var(--bb-tag); + font-family: 'Bebas Neue', sans-serif; + letter-spacing: 2px; + position: relative; +} + +.cityscape-shell.sunset .building-top { + border-bottom-color: rgba(255, 120, 40, 0.3); +} + +.building-top::after { + content: 'MY PROFILE'; + font-size: 12px; +} + +.building-main { + display: grid; + grid-template-columns: 80px 1fr 80px; + gap: 0; + padding: 20px; + min-height: 200px; + background: linear-gradient(90deg, rgba(0, 210, 255, 0.03) 0%, transparent 15%, transparent 85%, rgba(0, 210, 255, 0.03) 100%); + align-items: center; +} + +.cityscape-shell.sunset .building-main { + background: linear-gradient(90deg, rgba(255, 120, 40, 0.05) 0%, transparent 15%, transparent 85%, rgba(255, 120, 40, 0.05) 100%); +} + +.building-side { + display: flex; + flex-direction: column; + gap: 15px; + justify-content: center; +} + +.building-side.left-side { + border-right: 1px solid rgba(0, 210, 255, 0.2); + padding-right: 15px; +} + +.cityscape-shell.sunset .building-side.left-side { + border-right-color: rgba(255, 120, 40, 0.2); +} + +.building-side.right-side { + border-left: 1px solid rgba(0, 210, 255, 0.2); + padding-left: 15px; +} + +.cityscape-shell.sunset .building-side.right-side { + border-left-color: rgba(255, 120, 40, 0.2); +} + +.window { + width: 40px; + height: 40px; + background: linear-gradient(135deg, var(--win-cool) 0%, var(--win-cool) 100%); + border: 1px solid rgba(0, 210, 255, 0.5); + border-radius: 2px; + box-shadow: 0 0 10px rgba(0, 210, 255, 0.4), inset 0 0 8px rgba(0, 210, 255, 0.2); + position: relative; + animation: windowFlicker 4s ease-in-out infinite; +} + +.cityscape-shell.sunset .window { + background: linear-gradient(135deg, var(--win-warm) 0%, var(--win-warm) 100%); + border-color: rgba(255, 120, 40, 0.5); + box-shadow: 0 0 10px rgba(255, 120, 40, 0.4), inset 0 0 8px rgba(255, 120, 40, 0.2); + animation: windowFlickerSunset 4s ease-in-out infinite; +} + +@keyframes windowFlicker { + 0%, 100% { + box-shadow: 0 0 10px rgba(0, 210, 255, 0.4), inset 0 0 8px rgba(0, 210, 255, 0.2); + } + 50% { + box-shadow: 0 0 15px rgba(0, 210, 255, 0.6), inset 0 0 12px rgba(0, 210, 255, 0.3); + } +} + +@keyframes windowFlickerSunset { + 0%, 100% { + box-shadow: 0 0 10px rgba(255, 120, 40, 0.4), inset 0 0 8px rgba(255, 120, 40, 0.2); + } + 50% { + box-shadow: 0 0 15px rgba(255, 120, 40, 0.6), inset 0 0 12px rgba(255, 120, 40, 0.3); + } +} + +.building-center { + display: flex; + justify-content: center; + align-items: center; + padding: 0 20px; +} + +.player-display-window { + background: linear-gradient(135deg, rgba(0, 210, 255, 0.1) 0%, rgba(0, 210, 255, 0.05) 100%); + border: 2px solid var(--bb-tag); + border-radius: 4px; + padding: 30px; + text-align: center; + box-shadow: 0 0 20px rgba(0, 210, 255, 0.3), inset 0 0 15px rgba(0, 210, 255, 0.1); + min-width: 250px; +} + +.cityscape-shell.sunset .player-display-window { + background: linear-gradient(135deg, rgba(182, 64, 255, 0.1) 0%, rgba(182, 64, 255, 0.05) 100%); + box-shadow: 0 0 20px rgba(255, 64, 249, 0.3), inset 0 0 15px rgba(255, 64, 249, 0.1); +} + +.player-avatar { + font-size: 4rem; + margin-bottom: 15px; + display: block; + animation: avatarPulse 3s ease-in-out infinite; +} + +@keyframes avatarPulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +.player-name { + font-family: 'Bebas Neue', sans-serif; + font-size: 28px; + font-weight: bold; + color: var(--bb-title); + letter-spacing: 1px; + margin-bottom: 20px; + text-transform: uppercase; + background: linear-gradient(90deg, var(--bb-title), var(--bb-tag)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + cursor: pointer; + transition: all 0.3s ease; + padding: 8px 12px; + border-radius: 4px; + position: relative; +} + +.player-name:hover { + filter: brightness(1.2); + background: linear-gradient(90deg, var(--bb-tag), var(--bb-title)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + transform: scale(1.05); +} + +.player-name.copied { + background: linear-gradient(90deg, #4caf50, #66bb6a); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + box-shadow: 0 0 15px rgba(76, 175, 80, 0.5); +} + +.player-id-label { + font-family: 'Space Mono', monospace; + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--bb-tag); + margin-bottom: 8px; + opacity: 0.8; +} + +.player-id-value { + font-family: 'Space Mono', monospace; + font-size: 18px; + font-weight: 700; + letter-spacing: 2px; + color: var(--bb-tag); + background: rgba(0, 210, 255, 0.15); + padding: 12px 16px; + border: 1.5px solid var(--bb-tag); + border-radius: 2px; + word-break: break-all; + box-shadow: 0 0 15px rgba(0, 210, 255, 0.4); + text-shadow: 0 0 10px rgba(0, 210, 255, 0.5); + cursor: pointer; + transition: all 0.3s ease; + user-select: text; +} + +.cityscape-shell.sunset .player-id-value { + background: rgba(182, 64, 255, 0.15); + box-shadow: 0 0 15px rgba(255, 64, 249, 0.4); + text-shadow: 0 0 10px rgba(255, 64, 249, 0.5); +} + +.player-id-value:hover { + background: rgba(0, 210, 255, 0.25); + box-shadow: 0 0 20px rgba(0, 210, 255, 0.6); + transform: scale(1.02); +} + +.cityscape-shell.sunset .player-id-value:hover { + background: rgba(182, 64, 255, 0.25); + box-shadow: 0 0 20px rgba(255, 64, 249, 0.6); +} + +.player-id-value.copied { + background: rgba(76, 175, 80, 0.2); + border-color: #4caf50; + color: #4caf50; + box-shadow: 0 0 15px rgba(76, 175, 80, 0.5); +} + +.copy-notification { + font-family: 'Space Mono', monospace; + font-size: 12px; + font-weight: 700; + letter-spacing: 1px; + color: #4caf50; + margin-top: 12px; + animation: copyNotification 2s ease-in-out forwards; +} + +@keyframes copyNotification { + 0% { + opacity: 1; + transform: translateY(0); + } + 80% { + opacity: 1; + transform: translateY(0); + } + 100% { + opacity: 0; + transform: translateY(-10px); + } +} + +.building-base { + padding: 20px; + background: linear-gradient(180deg, rgba(0, 210, 255, 0.05) 0%, rgba(0, 210, 255, 0.02) 100%); + border-top: 1px solid rgba(0, 210, 255, 0.2); +} + +.cityscape-shell.sunset .building-base { + background: linear-gradient(180deg, rgba(255, 120, 40, 0.05) 0%, rgba(255, 120, 40, 0.02) 100%); + border-top-color: rgba(255, 120, 40, 0.2); +} + +.stat-panel { + display: flex; + justify-content: space-around; + align-items: center; + gap: 20px; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1; +} + +.stat-label { + font-family: 'Space Mono', monospace; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; + color: var(--bb-tag); + opacity: 0.7; +} + +.stat-value { + font-family: 'Space Mono', monospace; + font-size: 14px; + font-weight: 700; + color: var(--bb-title); + text-align: center; +} + +.stat-divider { + width: 1px; + height: 30px; + background: rgba(0, 210, 255, 0.2); +} + +.cityscape-shell.sunset .stat-divider { + background: rgba(255, 120, 40, 0.2); +} + +.building-door { + height: 60px; + background: linear-gradient(90deg, var(--bldg-mid) 0%, var(--bldg-body) 50%, var(--bldg-mid) 100%); + border-top: 1px solid rgba(0, 210, 255, 0.2); + border-radius: 0 0 8px 8px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.cityscape-shell.sunset .building-door { + border-top-color: rgba(255, 120, 40, 0.2); +} + +.building-door::before { + content: ''; + position: absolute; + width: 90%; + height: 100%; + border: 1.5px solid rgba(0, 210, 255, 0.3); + border-radius: 4px; + background: linear-gradient(90deg, rgba(0, 210, 255, 0.05), rgba(0, 210, 255, 0.02)); +} + +.cityscape-shell.sunset .building-door::before { + border-color: rgba(255, 120, 40, 0.3); + background: linear-gradient(90deg, rgba(255, 120, 40, 0.05), rgba(255, 120, 40, 0.02)); +} + +.door-handle { + width: 12px; + height: 12px; + background: radial-gradient(circle at 30% 30%, rgba(0, 210, 255, 0.8), rgba(0, 210, 255, 0.4)); + border-radius: 50%; + box-shadow: 0 0 10px rgba(0, 210, 255, 0.6); + position: relative; + z-index: 2; + margin-left: 80px; + animation: handleGlow 2s ease-in-out infinite; +} + +.cityscape-shell.sunset .door-handle { + background: radial-gradient(circle at 30% 30%, rgba(255, 120, 40, 0.8), rgba(255, 120, 40, 0.4)); + box-shadow: 0 0 10px rgba(255, 120, 40, 0.6); + animation: handleGlowSunset 2s ease-in-out infinite; +} + +@keyframes handleGlow { + 0%, 100% { + box-shadow: 0 0 10px rgba(0, 210, 255, 0.6); + } + 50% { + box-shadow: 0 0 20px rgba(0, 210, 255, 0.9); + } +} + +@keyframes handleGlowSunset { + 0%, 100% { + box-shadow: 0 0 10px rgba(255, 120, 40, 0.6); + } + 50% { + box-shadow: 0 0 20px rgba(255, 120, 40, 0.9); + } +} + +.back-btn { + position: absolute; + top: -50px; + left: 0; + background: transparent; + color: var(--bb-title); + border: 1px solid var(--dlg-border); + border-radius: 2px; + padding: 0.5rem 1rem; + font-family: 'Space Mono', monospace; + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.2s ease; +} + +.back-btn:hover { + background: rgba(0, 210, 255, 0.1); + border-color: var(--bb-tag); + color: var(--bb-tag); + box-shadow: 0 0 15px rgba(0, 210, 255, 0.3); +} + +.cityscape-shell.sunset .back-btn:hover { + background: rgba(182, 64, 255, 0.1); + box-shadow: 0 0 15px rgba(255, 64, 249, 0.3); +} + +@media (max-width: 600px) { + .building-structure { + border-radius: 4px; + } + + .building-main { + grid-template-columns: 50px 1fr 50px; + padding: 15px; + gap: 8px; + } + + .window { + width: 30px; + height: 30px; + } + + .player-display-window { + min-width: 180px; + padding: 20px; + } + + .player-avatar { + font-size: 3rem; + margin-bottom: 10px; + } + + .player-name { + font-size: 20px; + margin-bottom: 15px; + } + + .player-id-value { + font-size: 14px; + padding: 8px 12px; + } + + .building-base { + padding: 15px; + } + + .stat-panel { + gap: 10px; + } + + .back-btn { + top: -45px; + font-size: 9px; + padding: 0.4rem 0.8rem; + } +} diff --git a/src/app/pages/profile/profile.component.html b/src/app/pages/profile/profile.component.html new file mode 100644 index 0000000..3a7e63b --- /dev/null +++ b/src/app/pages/profile/profile.component.html @@ -0,0 +1,97 @@ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+ @if (currentUser; as user) { + +
+
+ +
+ + +
+ +
+
+
+
+
+
+ + +
+
+
👤
+
{{ user.username }}
+
PLAYER ID
+
{{ user.id }}
+ @if (idCopied) { +
✓ COPIED
+ } + @if (usernameCopied) { +
✓ COPIED
+ } +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+ RATING + {{ user.rating }} +
+
+
+ MEMBER SINCE + {{ user.createdAt | date: 'MMM dd, yyyy' }} +
+
+
+ + +
+
+
+
+ + + +
+ } +
+
+ +
+
+
+
diff --git a/src/app/pages/profile/profile.component.ts b/src/app/pages/profile/profile.component.ts new file mode 100644 index 0000000..e9b6c32 --- /dev/null +++ b/src/app/pages/profile/profile.component.ts @@ -0,0 +1,127 @@ +import { Component, OnInit, DestroyRef, 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 { ThemeService } from '../../services/theme.service'; +import { CurrentUser } from '../../models/auth.models'; + +interface Star { + style: Record; +} + +interface BackgroundBuilding { + style: Record; +} + +@Component({ + selector: 'app-profile', + standalone: true, + imports: [CommonModule], + templateUrl: './profile.component.html', + styleUrl: './profile.component.css' +}) +export class ProfileComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + private readonly authService = inject(AuthService); + private readonly themeService = inject(ThemeService); + private readonly router = inject(Router); + + currentUser: CurrentUser | null = null; + isSunsetMode = false; + idCopied = false; + usernameCopied = false; + + stars: Star[] = []; + bgBuildings: BackgroundBuilding[] = []; + + ngOnInit(): void { + this.authService.currentUser$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((user) => { + this.currentUser = user; + if (!user) { + this.router.navigate(['']); + } + }); + + this.themeService.darkMode$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((isDarkMode) => { + this.isSunsetMode = !isDarkMode; + }); + + this.generateStars(220); + this.generateBackgroundBuildings(); + } + + goBack(): void { + this.router.navigate(['']); + } + + copyPlayerId(id: string): void { + navigator.clipboard.writeText(id).then(() => { + this.idCopied = true; + setTimeout(() => { + this.idCopied = false; + }, 2000); + }); + } + + copyUsername(username: string): void { + navigator.clipboard.writeText(username).then(() => { + this.usernameCopied = true; + setTimeout(() => { + this.usernameCopied = false; + }, 2000); + }); + } + + private generateStars(count: number): void { + this.stars = Array.from({ length: count }, () => { + const size = Math.random() * 2 + 0.5; + return { + style: { + width: `${size}px`, + height: `${size}px`, + left: `${Math.random() * 100}%`, + top: `${Math.random() * 62}%`, + '--d': `${(Math.random() * 3 + 1.5).toFixed(1)}s`, + '--dl': `${-(Math.random() * 6).toFixed(1)}s` + } + }; + }); + } + + private generateBackgroundBuildings(): void { + const specs = [ + { l: '0%', w: '7%', h: '30vh' }, + { l: '3%', w: '4%', h: '18vh' }, + { l: '7%', w: '5%', h: '22vh' }, + { l: '11%', w: '8%', h: '28vh' }, + { l: '15%', w: '6%', h: '20vh' }, + { l: '18.5%', w: '4%', h: '18vh' }, + { l: '22.5%', w: '6%', h: '26vh' }, + { l: '28%', w: '5%', h: '25vh' }, + { l: '32%', w: '4%', h: '15vh' }, + { l: '35.5%', w: '4.5%', h: '20vh' }, + { l: '42%', w: '5%', h: '28vh' }, + { l: '47%', w: '5%', h: '22vh' }, + { l: '50%', w: '7%', h: '30vh' }, + { l: '55%', w: '6%', h: '27vh' }, + { l: '60.5%', w: '5%', h: '24vh' }, + { l: '64.5%', w: '3.5%', h: '17vh' }, + { l: '70%', w: '6%', h: '23vh' }, + { l: '75%', w: '4%', h: '19vh' }, + { l: '80.5%', w: '4%', h: '21vh' }, + { l: '85.5%', w: '9%', h: '32vh' }, + { l: '88%', w: '5%', h: '20vh' }, + { l: '91%', w: '3%', h: '16vh' }, + { l: '94%', w: '6%', h: '27vh' } + ]; + + this.bgBuildings = specs.map((spec) => ({ + style: { left: spec.l, width: spec.w, height: spec.h } + })); + } +} diff --git a/src/app/pages/welcome/welcome.component.css b/src/app/pages/welcome/welcome.component.css index e11a5ea..7317008 100644 --- a/src/app/pages/welcome/welcome.component.css +++ b/src/app/pages/welcome/welcome.component.css @@ -1,656 +1,907 @@ -.welcome-shell { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--size-xl); - position: relative; +@import '../../button-template.css'; + +:host { + display: block; } -.theme-toggle-container { - position: absolute; - top: 20px; - right: 20px; - z-index: 100; -} +.cityscape-shell { + --sky-1: #04000f; + --sky-2: #0e0235; + --sky-3: #2d0860; + --sky-4: #5e1185; + --sky-5: #8b1270; + --horizon: #be356e; + --bldg-body: #090920; + --bldg-mid: #0e0e2a; + --bldg-lit: rgba(100, 60, 200, 0.12); + --win-off: #0a0a1e; + --win-cool: #7de8ff; + --win-warm: #ffe88a; + --win-glow-c: 0 0 6px #00d5ff, 0 0 16px rgba(0, 200, 255, 0.35); + --win-glow-w: 0 0 6px #ffcc30, 0 0 16px rgba(255, 190, 0, 0.35); + --bb-bg: rgba(8, 6, 28, 0.92); + --bb-border: #00d5ff; + --bb-glow: 0 0 18px rgba(0, 210, 255, 0.5), inset 0 0 10px rgba(0, 210, 255, 0.05); + --bb-tag: #00d5ff; + --bb-title: #d4f4ff; + --btn-bg: #00d5ff; + --btn-fg: #04000f; + --btn-glow: 0 0 14px rgba(0, 210, 255, 0.9); + --ground-top: #06060f; + --ground-bot: #020208; + --moon-vis: 1; + --sun-vis: 0; + --star-vis: 1; + --cloud-col: rgba(255, 255, 255, 0.06); + --cloud-col2: rgba(255, 255, 255, 0.04); + --bg-bldg-op: 0.65; + --haze-col: rgba(6, 6, 15, 0.7); + --dlg-bg: rgba(8, 6, 28, 0.95); + --dlg-border: #00d5ff; -.switch { - display: inline-block; - position: relative; -} - -.switch__input { - clip: rect(1px, 1px, 1px, 1px); - clip-path: inset(50%); - height: 1px; - width: 1px; - margin: -1px; + min-height: 100svh; + font-family: 'Space Mono', 'Courier New', monospace; overflow: hidden; - padding: 0; - position: absolute; -} - -.switch__label { + user-select: none; position: relative; - display: inline-block; - width: 120px; - height: 60px; - background-color: #2B2B2B; - border: 5px solid #5B5B5B; - border-radius: 9999px; - cursor: pointer; - transition: all 0.4s cubic-bezier(.46,.03,.52,.96); + background: var(--sky-1); } -.switch__indicator { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) translateX(-72%); - display: block; - width: 40px; - height: 40px; - background-color: #7B7B7B; - border-radius: 9999px; - box-shadow: 10px 0px 0 0 rgba(0, 0, 0, 0.2) inset; - transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +.cityscape-shell.sunset { + --sky-1: #4d3279; + --sky-2: #5A5485; + --sky-3: #996C96; + --sky-4: #e85040; + --sky-5: #f07020; + --horizon: #ffaa30; + --bldg-body: #13072a; + --bldg-mid: #1e0e38; + --bldg-lit: rgba(255, 120, 40, 0.14); + --win-off: #18082e; + --win-cool: #ffcc55; + --win-warm: #ff7730; + --win-glow-c: 0 0 6px #ea00ff, 0 0 16px rgba(255, 180, 0, 0.4); + --win-glow-w: 0 0 6px #ff00e6, 0 0 16px rgba(255, 196, 0, 0.4); + --bb-bg: rgba(20, 5, 45, 0.93); + --bb-border: #ff40f9; + --bb-glow: 0 0 18px rgba(255, 50, 217, 0.55), inset 0 0 10px rgba(255, 120, 30, 0.06); + --bb-tag: #b640ff; + --bb-title: #ffe0c0; + --btn-bg: #f26ae2; + --btn-fg: #13072a; + --btn-glow: 0 0 14px rgba(238, 50, 255, 0.9); + --ground-top: #0a0318; + --ground-bot: #04010a; + --moon-vis: 0; + --sun-vis: 1; + --star-vis: 0; + --cloud-col: rgba(255, 160, 100, 0.22); + --cloud-col2: rgba(255, 100, 60, 0.14); + --bg-bldg-op: 0.45; + --haze-col: rgba(10, 3, 24, 0.65); + --dlg-bg: rgba(20, 5, 45, 0.96); + --dlg-border: #ff40cf; } -.switch__indicator::before, -.switch__indicator::after { - position: absolute; - content: ''; - display: block; - background-color: #FFFFFF; - border-radius: 9999px; - transition: all 0.4s cubic-bezier(.46,.03,.52,.96); -} - -.switch__indicator::before { - top: 7px; - left: 7px; - width: 9px; - height: 9px; - opacity: 0.6; -} - -.switch__indicator::after { - bottom: 8px; - right: 6px; - width: 14px; - height: 14px; - opacity: 0.8; -} - -.switch__decoration { - position: absolute; - top: 65%; - left: 50%; - display: block; - width: 5px; - height: 5px; - background-color: #FFFFFF; - border-radius: 9999px; - animation: twinkle-stars 0.8s infinite -0.6s; - transition: all 0.4s cubic-bezier(.46,.03,.52,.96); -} - -.switch__decoration::before, -.switch__decoration::after { - position: absolute; - display: block; - content: ''; - width: 5px; - height: 5px; - background-color: #FFFFFF; - border-radius: 9999px; -} - -.switch__decoration::before { - top: -20px; - left: 10px; - opacity: 1; - animation: twinkle-stars 0.6s infinite; -} - -.switch__decoration::after { - top: -7px; - left: 30px; - animation: twinkle-stars 0.6s infinite -0.2s; -} - -@keyframes twinkle-stars { - 50% { opacity: 0.2; } -} - -.switch__input:checked + .switch__label { - background-color: #8FB5F5; - border-color: #347CF8; -} - -.switch__input:checked + .switch__label .switch__indicator { - background-color: #ECD21F; - box-shadow: none; - transform: translate(-50%, -50%) translateX(72%); -} - -.switch__input:checked + .switch__label .switch__indicator::before, -.switch__input:checked + .switch__label .switch__indicator::after { - display: none; -} - -.switch__input:checked + .switch__label .switch__decoration { - top: 50%; - transform: translate(0%, -50%); - animation: cloud 8s linear infinite; - width: 20px; - height: 20px; -} - -.switch__input:checked + .switch__label .switch__decoration::before { - width: 10px; - height: 10px; - top: auto; - bottom: 0; - left: -8px; - animation: none; -} - -.switch__input:checked + .switch__label .switch__decoration::after { - width: 15px; - height: 15px; - top: auto; - bottom: 0; - left: 16px; - animation: none; -} - -.switch__input:checked + .switch__label .switch__decoration, -.switch__input:checked + .switch__label .switch__decoration::before, -.switch__input:checked + .switch__label .switch__decoration::after { - border-radius: 9999px 9999px 0 0; -} - -.switch__input:checked + .switch__label .switch__decoration::after { - border-bottom-right-radius: 9999px; -} - -@keyframes cloud { - 0% { transform: translate(0%, -50%); } - 50% { transform: translate(-50%, -50%); } - 100% { transform: translate(0%, -50%); } -} - -.clouds-container { - display: flex; - justify-content: center; - align-items: flex-start; - gap: 60px; +.scene { + position: relative; width: 100%; - max-width: 900px; - margin-bottom: var(--size-xl); - position: relative; - z-index: 1; + height: 100svh; + overflow: hidden; } -.plane { - position: relative; - width: 320px; - height: 140px; - display: flex; - align-items: center; - justify-content: center; - filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.12)); -} - -.plane-body { - width: 240%; - height: 240%; - object-fit: contain; +.sky { position: absolute; + inset: 0; + background: linear-gradient(180deg, var(--sky-1) 0%, var(--sky-2) 22%, var(--sky-3) 48%, var(--sky-4) 70%, var(--sky-5) 85%, var(--horizon) 100%); + transition: background 1.6s ease; } -.plane-gif { - width: 70px; - height: 70px; - object-fit: contain; - z-index: 10; - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15)); - position: relative; - transform: translateX(-25px) translateY(15px); +.stars-layer { + position: absolute; + inset: 0; + pointer-events: none; + opacity: var(--star-vis); + transition: opacity 1.6s ease; +} + +.star { + position: absolute; + border-radius: 50%; + background: #ffffff; + animation: twinkle var(--d, 3s) ease-in-out infinite var(--dl, 0s); +} + +@keyframes twinkle { + 0%, + 100% { + opacity: 0.15; + transform: scale(0.7); + } + + 50% { + opacity: 1; + transform: scale(1.3); + } +} + +.moon { + position: absolute; + top: 9%; + right: 14%; + width: 52px; + height: 52px; + border-radius: 50%; + background: radial-gradient(circle at 36% 34%, #fffbe8, #f5d060 60%, #c8a020); + box-shadow: 0 0 28px rgba(245, 210, 80, 0.55), 0 0 70px rgba(240, 190, 40, 0.25); + opacity: var(--moon-vis); + transition: opacity 1.6s ease; +} + +.sun { + position: absolute; + bottom: 12%; + left: 50%; + transform: translateX(-50%); + width: 76px; + height: 76px; + border-radius: 50%; + background: radial-gradient(circle at 50% 50%, #fffce0, #ffd020 40%, #ff9000); + box-shadow: 0 0 40px rgba(255, 200, 0, 0.85), 0 0 90px rgba(255, 150, 0, 0.45), 0 0 200px rgba(255, 80, 0, 0.2); + opacity: var(--sun-vis); + transition: opacity 1.6s ease; + z-index: 4; +} + +.cloud-wrap { + position: absolute; + top: 4%; + left: 0; + right: 0; + height: 18%; + pointer-events: none; } .cloud { - position: relative; + position: absolute; + border-radius: 60px; + filter: blur(18px); + transition: background 1.6s ease; +} + +.cloud-a { + width: 280px; + height: 55px; + background: var(--cloud-col); + top: 10px; + left: 4%; + animation: drift 50s ease-in-out infinite; +} + +.cloud-b { + width: 200px; + height: 44px; + background: var(--cloud-col2); + top: 28px; + left: 15%; + animation: drift 38s ease-in-out infinite 8s; +} + +.cloud-c { + width: 360px; + height: 65px; + background: var(--cloud-col); + top: 5px; + left: 36%; + animation: drift 62s ease-in-out infinite 3s; +} + +.cloud-d { width: 220px; - height: 90px; + height: 50px; + background: var(--cloud-col2); + top: 20px; + left: 62%; + animation: drift 44s ease-in-out infinite 14s; +} + +.cloud-e { + width: 180px; + height: 40px; + background: var(--cloud-col); + top: 35px; + left: 80%; + animation: drift 56s ease-in-out infinite 6s; +} + +@keyframes drift { + 0%, + 100% { + transform: translateX(0); + } + + 50% { + transform: translateX(-50px); + } +} + +.bg-layer { + position: absolute; + bottom: 8vh; + left: 0; + right: 0; + z-index: 5; + pointer-events: none; + opacity: var(--bg-bldg-op); + transition: opacity 1.6s ease; +} + +.bg-bldg { + position: absolute; + bottom: 0; + background: var(--bldg-body); + transition: background 1.6s ease; +} + +.bg-bldg::before { + content: ''; + position: absolute; + inset: 0; + background-image: repeating-linear-gradient(0deg, transparent, transparent 14px, rgba(120, 120, 200, 0.08) 14px, rgba(120, 120, 200, 0.08) 16px), + repeating-linear-gradient(90deg, transparent, transparent 12px, rgba(120, 120, 200, 0.08) 12px, rgba(120, 120, 200, 0.08) 14px); +} + +.main-layer { + position: absolute; + bottom: 8vh; + left: 0; + right: 0; + z-index: 10; +} + +.bwrap { + position: absolute; + bottom: 0; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.playerone-gif { + position: absolute; + top: 36px; + left: 50%; + width: 18px; + height: 18px; + transform: translateX(-50%); + object-fit: contain; + z-index: 14; + pointer-events: none; + image-rendering: auto; +} + +.bpart { + position: relative; + background: var(--bldg-body); + transition: background 1.6s ease; + overflow: hidden; + flex-shrink: 0; +} + +.bpart::after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 28%; + background: linear-gradient(90deg, transparent, var(--bldg-lit)); + pointer-events: none; + transition: background 1.6s ease; +} + +.wins { + display: grid; + gap: 3px; + padding: 5px; + pointer-events: none; +} + +.w { + height: 9px; + border-radius: 1px; + background: var(--win-off); + transition: background 0.4s ease, box-shadow 0.4s ease; +} + +.w.lc { + background: var(--win-cool); + box-shadow: var(--win-glow-c); + animation: wflicker var(--wd, 5s) ease-in-out infinite var(--wdl, 0s); +} + +.w.lw { + background: var(--win-warm); + box-shadow: var(--win-glow-w); + animation: wflicker var(--wd, 6s) ease-in-out infinite var(--wdl, 0s); +} + +@keyframes wflicker { + 0%, + 88%, + 92%, + 97%, + 100% { + opacity: 1; + } + + 90% { + opacity: 0.7; + } + + 95% { + opacity: 0.85; + } +} + +.bb { + margin: 0 7px 5px; + padding: 10px 12px; + background: var(--bb-bg); + border: 1.5px solid var(--bb-border); + box-shadow: var(--bb-glow); + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + text-align: center; + pointer-events: auto; + transition: border-color 1.6s ease, box-shadow 1.6s ease, background 1.6s ease; +} + +.bb-tag { + font-family: 'Bebas Neue', sans-serif; + font-size: 10px; + letter-spacing: 3px; + color: var(--bb-tag); + opacity: 0.75; + transition: color 1.6s ease; +} + +.bb-title { + font-family: 'Bebas Neue', sans-serif; + font-size: clamp(13px, 1.4vw, 20px); + line-height: 1.1; + color: var(--bb-title); + transition: color 1.6s ease; +} + +.bb-subtitle { + font-family: 'Space Mono', monospace; + font-size: 9px; + color: var(--bb-title); + opacity: 0.55; + letter-spacing: 0.5px; + transition: color 1.6s ease; +} + +.bb-btn { + background: var(--btn-bg); + color: var(--btn-fg); + border: none; + border-radius: 2px; + padding: 5px 14px; + font-family: 'Space Mono', monospace; + font-size: 10px; + 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; +} + +.bb-btn:hover:enabled { + transform: scale(1.08); + filter: brightness(1.15); +} + +.bb-btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.neon { + font-family: 'Bebas Neue', sans-serif; + font-size: 11px; + letter-spacing: 2.5px; + color: var(--bb-tag); + border: 1px solid var(--bb-border); + padding: 2px 7px; + border-radius: 2px; + text-shadow: 0 0 8px currentColor, 0 0 20px currentColor; + box-shadow: 0 0 6px currentColor; + animation: nflicker 9s ease-in-out infinite; + display: inline-block; + transition: color 1.6s ease, border-color 1.6s ease; +} + +@keyframes nflicker { + 0%, + 94%, + 96%, + 100% { + opacity: 1; + } + + 95% { + opacity: 0.25; + } + + 98% { + opacity: 0.5; + } +} + +.ant { + width: 3px; + background: var(--bldg-mid); + margin: 0 auto; + transition: background 1.6s ease; + position: relative; + flex-shrink: 0; +} + +.ant::after { + content: ''; + position: absolute; + top: -5px; + left: 50%; + transform: translateX(-50%); + width: 8px; + height: 8px; + border-radius: 50%; + background: #ff2222; + box-shadow: 0 0 10px #ff2222, 0 0 20px rgba(255, 0, 0, 0.4); + animation: blink 1.6s step-start infinite; +} + +@keyframes blink { + 0%, + 49% { + opacity: 1; + } + + 50%, + 100% { + opacity: 0; + } +} + +.ground { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 10vh; + background: linear-gradient(180deg, var(--ground-top), var(--ground-bot)); + z-index: 20; + transition: background 1.6s ease; +} + +.haze { + position: absolute; + bottom: 8vh; + left: 0; + right: 0; + height: 12vh; + background: linear-gradient(0deg, var(--haze-col), transparent); + z-index: 15; + pointer-events: none; + transition: background 1.6s ease; +} + +.tgl { + position: fixed; + top: 20px; + right: 20px; + z-index: 200; + display: inline-grid; + place-items: center; + width: 56px; + height: 56px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 50%; + padding: 0; + color: rgba(255, 255, 255, 0.85); + cursor: pointer; + backdrop-filter: blur(12px); + transition: background 0.3s, transform 0.2s; +} + +.tgl:hover { + background: rgba(255, 255, 255, 0.14); + transform: scale(1.04); +} + +.tgl-icon { + font-size: 22px; + line-height: 1; +} + +.mode-badge { + position: fixed; + top: 20px; + left: 20px; + z-index: 200; + font-family: 'Bebas Neue', sans-serif; + font-size: 13px; + letter-spacing: 4px; + color: rgba(255, 255, 255, 0.45); + pointer-events: none; +} + +.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-actions { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.dialog-btn { + flex: 1; + min-width: 120px; + background: var(--btn-bg); + color: var(--btn-fg); + border: none; + border-radius: 2px; + padding: 0.55rem 0.75rem; + font-family: 'Space Mono', monospace; + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; + cursor: pointer; + box-shadow: var(--btn-glow); +} + +.dialog-btn:hover:enabled { + filter: brightness(1.12); +} + +.dialog-btn:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.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; +} + +.import-mode-group { + display: flex; + gap: 0.8rem; +} + +.import-mode-option { + display: inline-flex; + align-items: center; + gap: 0.35rem; + color: var(--bb-title); + font-size: 13px; +} + +.error-banner { + position: fixed; + left: 50%; + bottom: 1rem; + transform: translateX(-50%); + z-index: 360; + margin: 0; + color: #ffd0d0; + background: rgba(146, 0, 16, 0.45); + border: 1px solid rgba(255, 170, 170, 0.6); + border-radius: 9px; + padding: 0.5rem 0.7rem; + font-size: 12px; + font-weight: 600; +} + +/* Speech Bubble Styles */ +.speech-bubble-container { + position: fixed; + top: 35%; + left: 55%; + transform: translate(-50%, -50%); + z-index: 500; + cursor: pointer; + animation: slideInBubble 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes slideInBubble { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.5); + } + 100% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.speech-bubble { + background: linear-gradient(135deg, #B9DAD1 0%, #B9C2DA 100%); + border: 2px solid #8b1270; + border-radius: 20px; + padding: 16px 24px; + font-family: 'Comic Sans MS', 'Comic Sans', cursive; + font-size: 18px; + font-weight: bold; + color: #5A2C28; + white-space: nowrap; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2), + inset 0 1px 3px rgba(255, 255, 255, 0.3); + position: relative; + transition: all 0.3s ease; +} + +.speech-bubble:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), + inset 0 1px 3px rgba(255, 255, 255, 0.5); +} + +.bubble-text { + margin: 0; +} + +.bubble-tail { + position: absolute; + bottom: -12px; + left: 20px; + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 0px solid transparent; + border-top: 12px solid #B9DAD1; +} + +/* Zoom Overlay and Window */ +.zoom-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; display: flex; align-items: center; justify-content: center; - filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.12)); + animation: fadeIn 0.3s ease; + cursor: pointer; } -.cloud::before { - content: ''; - position: absolute; - width: 90px; - height: 90px; - background: #ffffff; - border-radius: 50%; - top: 0; - left: 0; - box-shadow: 55px 0 0 9px #ffffff, 110px 0 0 5px #ffffff, 27px -18px 0 13px #ffffff, 82px -13px 0 11px #ffffff; +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } } -.cloud::after { - content: ''; - position: absolute; - width: 220px; - height: 45px; - background: #ffffff; - border-radius: 50px; - bottom: 0; - left: 0; +.zoom-window-wrapper { + cursor: auto; + animation: zoomInWindow 1.2s cubic-bezier(0.34, 1.56, 0.64, 1); } -.cloud-gif { - width: 150px; - height: 100px; - object-fit: contain; - z-index: 10; - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15)); +@keyframes zoomInWindow { + 0% { + transform: scale(0.1); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.zoom-window-frame { + background: #13072a; + border: 8px solid #f26ae2; + border-radius: 16px; + padding: 40px 20px 20px 20px; + box-shadow: 0 0 40px rgba(242, 106, 226, 0.6), + inset 0 0 20px rgba(242, 106, 226, 0.2); + max-width: 90vw; + max-height: 90vh; position: relative; } -.gif-with-halo { +.zoom-player-2 { position: relative; - display: inline-block; + display: flex; + align-items: center; + justify-content: center; } -.gif-with-halo::before { - content: ''; +.player-2-gif { + max-width: 100%; + max-height: 70vh; + width: auto; + height: auto; + display: block; + border-radius: 12px; + cursor: pointer; + transition: transform 0.2s ease; +} + +.player-2-gif:hover { + transform: scale(1.02); +} + +.second-speech-bubble { position: absolute; - top: -10px; + top: -60px; left: 50%; transform: translateX(-50%); - width: 75px; - height: 15px; - border: 2px solid rgba(255, 215, 0, 0.7); - border-radius: 50%; - box-shadow: 0 0 8px rgba(255, 215, 0, 0.5); - z-index: 5; + background: linear-gradient(135deg, #C19EF5 0%, #E1EAA9 100%); + border: 2px solid #BA6D4B; + border-radius: 20px; + padding: 12px 18px; + font-family: 'Comic Sans MS', 'Comic Sans', cursive; + font-size: 16px; + font-weight: bold; + color: #5A2C28; + white-space: nowrap; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2), + inset 0 1px 3px rgba(255, 255, 255, 0.3); + animation: popInBubble 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 10; } -.welcome-card { - width: min(900px, 100%); - border-radius: var(--border-radius-lg); - border: var(--border-width) solid var(--color-border); - background: var(--color-bg-main); - padding: var(--size-xl-padding); - box-shadow: var(--shadow-md); -} - -h1 { - margin: 0 0 var(--size-xs); - color: var(--color-text-primary); - font-size: var(--heading-h1); -} - -p { - margin: 0 0 var(--size-lg); -} - -.mode-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--size-xl-gap); -} - -.mode { - border: var(--border-width) solid var(--color-border); - border-radius: var(--border-radius-md); - padding: var(--size-lg-padding); - text-align: left; - display: grid; - gap: var(--size-xs); -} - -.mode span { - font-size: 1.15rem; - color: var(--color-text-primary); -} - -.mode small { - color: var(--color-text-primary); - opacity: 0.9; -} - -.mode-active { - background: var(--color-secondary-mint); - cursor: pointer; -} - -.mode-active:hover:enabled { - background: var(--color-secondary-blue); -} - -.mode-disabled { - background: var(--color-bg-card); - opacity: 0.75; -} - -.error { - color: var(--color-text-primary); - font-weight: 700; - margin-top: var(--size-xl); -} - -.plane-left { - animation: float 3s ease-in-out infinite; -} - -.cloud-left { - animation: float 3s ease-in-out infinite; - animation-delay: 0.25s; -} - -.cloud-right { - animation: float 3s ease-in-out infinite; - animation-delay: 0.5s; -} - -@keyframes float { - 0%, 100% { - transform: translateY(0px); +@keyframes popInBubble { + 0% { + opacity: 0; + transform: translateX(-50%) scale(0.3); } - 50% { - transform: translateY(-15px); + 100% { + opacity: 1; + transform: translateX(-50%) scale(1); } } -@media (max-width: 768px) { - .theme-toggle-container { - top: 10px; - right: 10px; - } - - .switch__label { - width: 100px; - height: 50px; - } - - .switch__indicator { - width: 33px; - height: 33px; - } - - .welcome-shell { - padding: var(--size-lg); - } - - .clouds-container { - gap: 30px; - margin-bottom: var(--size-lg); - } - - .plane { - width: 240px; - height: 110px; - } - - .plane-gif { - width: 55px; - height: 55px; - transform: translateX(-20px) translateY(12px); - } - - .cloud { - width: 170px; - height: 75px; - } - - .cloud::before { - width: 65px; - height: 65px; - box-shadow: 40px 0 0 7px #ffffff, 80px 0 0 3px #ffffff, 20px -13px 0 10px #ffffff, 60px -10px 0 8px #ffffff; - } - - .cloud::after { - width: 170px; - height: 38px; - } - - .cloud-gif { - width: 140px; - height: 140px; - } - - .gif-with-halo::before { - top: -8px; - width: 80px; - height: 12px; - border: 1.5px solid rgba(255, 215, 0, 0.7); - } - - .welcome-card { - padding: var(--size-xl); - } - - h1 { - font-size: var(--heading-h1-mobile); - } - - p { - font-size: 0.875rem; - margin: 0 0 var(--size-lg); - } - - .mode-grid { - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: var(--size-md-gap); - } - - .mode { - padding: var(--size-md-padding); - gap: var(--size-xs-gap); - } - - .mode span { - font-size: 1rem; - } - - .mode small { - font-size: 0.7rem; - } - - .error { - font-size: 0.75rem; - } +.second-speech-bubble .bubble-tail { + top: 100%; + bottom: auto; + left: 50%; + transform: translateX(-50%); + border-top: 12px solid #C19EF5; } -@media (max-width: 480px) { - .welcome-shell { - padding: var(--size-sm); - } - - .welcome-card { - padding: var(--size-lg-padding); - border-radius: var(--border-radius-md); - } - - h1 { - font-size: var(--heading-h1-small); - margin: 0 0 var(--size-xs); - } - - p { - font-size: 0.8rem; - margin: 0 0 var(--size-md); - } - - .mode-grid { - grid-template-columns: 1fr; - } - - .mode { - padding: var(--size-md-padding); - } - - .mode span { - font-size: 0.95rem; - } +/* Happy Meow Bubble */ +.happy-speech-bubble { + position: absolute; + top: -60px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #F3C8A0 0%, #BA6D4B 100%); + border: 2px solid #5A2C28; + border-radius: 20px; + padding: 12px 18px; + font-family: 'Comic Sans MS', 'Comic Sans', cursive; + font-size: 16px; + font-weight: bold; + color: #fff; + white-space: nowrap; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), + inset 0 1px 3px rgba(255, 255, 255, 0.4), + 0 0 20px rgba(243, 200, 160, 0.5); + animation: popInBubble 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 10; } -.difficulty-selector { - grid-column: 1 / -1; - background: var(--color-bg-card); - border: var(--border-width) solid var(--color-border); - border-radius: var(--border-radius-md); - padding: var(--size-lg-padding); - margin: var(--size-md) 0; +.happy-speech-bubble .bubble-tail { + top: 100%; + bottom: auto; + left: 50%; + transform: translateX(-50%); + border-top: 12px solid #F3C8A0; } -.difficulty-selector p { - margin: 0 0 var(--size-md); - font-weight: 600; -} - -.difficulty-buttons { +/* Meat Emoji */ +.meat-emoji { + position: fixed; + font-size: 48px; + cursor: grab; + user-select: none; + z-index: 1001; display: flex; - gap: var(--size-md-gap); - flex-wrap: wrap; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + animation: meatAppear 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3)); + transition: transform 0.1s ease; } -.difficulty-btn { - flex: 1; - min-width: 80px; - padding: var(--size-md-padding); - border: var(--border-width) solid var(--color-border); - border-radius: var(--border-radius-md); - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; +.meat-emoji:active { + cursor: grabbing; + transform: scale(1.1); + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)); } -.difficulty-btn.easy { - background: var(--color-success-light, #d4edda); - color: var(--color-text-primary); +@keyframes meatAppear { + 0% { + opacity: 0; + transform: scale(0); + } + 100% { + opacity: 1; + transform: scale(1); + } } -.difficulty-btn.easy:hover:enabled { - background: var(--color-success, #28a745); - color: white; +@media (max-width: 900px) { + .bwrap { + transform: scale(0.9); + transform-origin: bottom center; + } } -.difficulty-btn.medium { - background: var(--color-warning-light, #fff3cd); - color: var(--color-text-primary); -} +@media (max-width: 700px) { + .scene { + transform: scale(0.8); + transform-origin: bottom center; + width: 125%; + margin-left: -12.5%; + } -.difficulty-btn.medium:hover:enabled { - background: var(--color-warning, #ffc107); - color: var(--color-text-primary); -} - -.difficulty-btn.hard { - background: var(--color-danger-light, #f8d7da); - color: var(--color-text-primary); -} - -.difficulty-btn.hard:hover:enabled { - background: var(--color-danger, #dc3545); - color: white; -} - -.join-game-form { - grid-column: 1 / -1; - background: var(--color-bg-card); - border: var(--border-width) solid var(--color-border); - border-radius: var(--border-radius-md); - padding: var(--size-lg-padding); - margin: var(--size-md) 0; -} - -.join-game-form p { - margin: 0 0 var(--size-md); - font-weight: 600; -} - -.join-game-input-group { - display: flex; - gap: var(--size-md-gap); - flex-wrap: wrap; -} - -.join-game-input { - flex: 1; - min-width: 150px; - padding: var(--size-md-padding); - border: var(--border-width) solid var(--color-border); - border-radius: var(--border-radius-md); - background: white; - color: var(--color-text-primary); - font-family: inherit; - font-size: 1rem; - transition: border-color 0.3s ease; -} - -.join-game-input:focus { - outline: none; - border-color: var(--color-secondary-mint); - box-shadow: 0 0 0 3px rgba(185, 218, 209, 0.2); -} - -.join-game-input:disabled { - background: var(--color-bg-card); - opacity: 0.6; -} - -.join-game-btn { - padding: var(--size-md-padding) var(--size-lg); - border: var(--border-width) solid var(--color-border); - border-radius: var(--border-radius-md); - cursor: pointer; - font-weight: 600; - font-size: 0.95rem; - transition: all 0.3s ease; -} - -.join-game-btn.join { - background: var(--color-secondary-mint); - color: var(--color-text-primary); -} - -.join-game-btn.join:hover:enabled { - background: var(--color-secondary-blue); -} - -.join-game-btn.join:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.join-game-btn.cancel { - background: var(--color-bg-card); - color: var(--color-text-primary); -} - -.join-game-btn.cancel:hover:enabled { - background: var(--color-border); -} - -.join-game-btn.cancel:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.difficulty-btn.hard:hover:enabled { - background: var(--color-danger, #dc3545); - color: white; -} - -.difficulty-btn:disabled { - opacity: 0.6; - cursor: not-allowed; + .dialog-card { + width: min(380px, 100%); + } } diff --git a/src/app/pages/welcome/welcome.component.html b/src/app/pages/welcome/welcome.component.html index d5b67d8..ec83f91 100644 --- a/src/app/pages/welcome/welcome.component.html +++ b/src/app/pages/welcome/welcome.component.html @@ -1,99 +1,262 @@ -
-
-
- - -
-
-
-
- Player One -
-
- Plane - Raf -
-
-
- Player Two +
+ +
+
+
+
+
+
+
+
+
+
+
+
-
-
-

Welcome to NowChess

-

Pick a mode to begin.

-
- +
+
+
- @if (showDifficultySelector) { -
-

Select difficulty:

-
- - - +
+
+
+
+
+
- } - - - - -
- - @if (showJoinGameForm) { -
-

Enter the game ID:

-
- - - +
+
+
+
+
+
+
+
+
JOIN
+
JOIN
GAME
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ OPEN 24/7 +
+
+
BOT
+
PLAY WITH
A BOT
+ +
+
+
+
+
+
+
+ +
+ Player One +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
WELCOME
+
WELCOME TO
NOWCHESS
+
Play your next move from the skyline.
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ MORE +
+
+
OPTIONS
+
MORE
OPTIONS
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + @if (showSpeechBubble) { +
+
+
{{ bubbleMessage }}
+
+
+
} - @if (errorMessage) { -

{{ errorMessage }}

+ + @if (isZoomedIn) { +
+
+
+
+ Player 2 + @if (showSecondSpeechBubble) { +
+
Feed me! 🍖
+
+
+ } + @if (showHappyBubble) { +
+
Happy meow! 😸
+
+
+ } +
+
+ + + @if (showMeatEmoji) { +
+ 🍖 +
+ } +
+
} -
-
+ +
+
+ + + @if (showDifficultyDialog) { +
+
+
SELECT DIFFICULTY
+
+ + + +
+
+
+ } + + @if (showOptionsDialog) { +
+
+
MORE OPTIONS
+
+ +
+
+
+ } + + @if (showJoinDialog) { +
+
+
JOIN GAME
+ +
+ + +
+
+
+ } + + @if (showImportDialog) { +
+
+
IMPORT GAME
+
+ + +
+ +
+ + +
+
+
+ } + + @if (errorMessage) { +

{{ errorMessage }}

+ } + \ No newline at end of file diff --git a/src/app/pages/welcome/welcome.component.ts b/src/app/pages/welcome/welcome.component.ts index 3f3c149..749de98 100644 --- a/src/app/pages/welcome/welcome.component.ts +++ b/src/app/pages/welcome/welcome.component.ts @@ -1,41 +1,342 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { finalize } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getErrorMessage } from '../../core/http/error-message.util'; +import { CurrentUser } from '../../models/auth.models'; +import { AuthDialogService } from '../../services/auth-dialog.service'; +import { AuthService } from '../../services/auth.service'; import { GameApiService } from '../../services/game-api.service'; +import { ThemeService } from '../../services/theme.service'; + +type Difficulty = 'easy' | 'medium' | 'hard'; +type ImportMode = 'fen' | 'pgn'; + +interface Star { + style: Record; +} + +interface BackgroundBuilding { + style: Record; +} + +interface WindowCell { + state: 'off' | 'on'; + color?: string; + glowColor?: string; + style: Record; +} @Component({ selector: 'app-welcome', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './welcome.component.html', - styleUrl: './welcome.component.css' + styleUrls: ['./welcome.component.css'] }) -export class WelcomeComponent { +export class WelcomeComponent implements OnInit, OnDestroy { + private readonly destroyRef = inject(DestroyRef); + private readonly authService = inject(AuthService); + private readonly authDialogService = inject(AuthDialogService); + private readonly themeService = inject(ThemeService); + creating = false; - errorMessage = ''; - showDifficultySelector = false; - showJoinGameForm = false; - gameIdInput = ''; joiningGame = false; + importing = false; + errorMessage = ''; + + showDifficultyDialog = false; + showOptionsDialog = false; + showJoinDialog = false; + showImportDialog = false; + + gameIdInput = ''; + importMode: ImportMode = 'fen'; + importText = ''; + + isSunsetMode = false; + modeBadge = 'NIGHT MODE'; + currentUser: CurrentUser | null = null; + private authDialogState: 'login' | 'register' | null = null; + private pendingAction: (() => void) | null = null; + + // Speech bubble and zoom features + showSpeechBubble = false; + isZoomedIn = false; + showSecondSpeechBubble = false; + showHappyBubble = false; + showMeatEmoji = false; + bubbleMessage = 'meow'; + + // Meat emoji drag state + meatX = 0; + meatY = 0; + isDraggingMeat = false; + meatDragOffsetX = 0; + meatDragOffsetY = 0; + + stars: Star[] = []; + bgBuildings: BackgroundBuilding[] = []; + windows: Record = {}; + + private flickerIntervalId: ReturnType | undefined; + private speechBubbleTimeoutId: ReturnType | undefined; + private zoomTimeoutId: ReturnType | undefined; + + private coolColors = ['#7de8ff', '#00d5ff', '#5bc0de', '#31b0d5', '#4fc3f7', '#29b6f6']; + private coolGlowColors = ['#00d5ff', '#00d5ff', '#31b0d5', '#31b0d5', '#03a9f4', '#0288d1']; + + private warmColors = ['#ffe88a', '#ffcc30', '#f0ad4e', '#ec971f', '#ffb74d', '#ffa726']; + private warmGlowColors = ['#ffcc30', '#ffcc30', '#ec971f', '#ec971f', '#ff9800', '#fb8c00']; constructor( private readonly router: Router, private readonly gameApi: GameApiService ) { - this.initTheme(); } - private initTheme(): void { - const savedTheme = localStorage.getItem('theme'); - if (savedTheme === 'dark') { - document.documentElement.setAttribute('data-theme', 'dark'); + ngOnInit(): void { + this.themeService.darkMode$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((isDarkMode) => { + this.isSunsetMode = !isDarkMode; + this.modeBadge = this.isSunsetMode ? 'SUNSET MODE' : 'NIGHT MODE'; + }); + + this.authService.currentUser$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((user) => { + this.currentUser = user; + this.maybeRunPendingAction(); + }); + + this.authDialogService.dialogState$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((state) => { + this.authDialogState = state; + this.maybeRunPendingAction(); + }); + + this.generateStars(220); + this.generateBackgroundBuildings(); + this.generateWindowsForAllBuildings(); + this.startWindowFlicker(); + + // Show speech bubble after 5 seconds + this.speechBubbleTimeoutId = setTimeout(() => { + this.showSpeechBubble = true; + }, 5000); + } + + ngOnDestroy(): void { + this.stopWindowFlicker(); + if (this.speechBubbleTimeoutId) { + clearTimeout(this.speechBubbleTimeoutId); + } + if (this.zoomTimeoutId) { + clearTimeout(this.zoomTimeoutId); } } + openDifficultyDialog(): void { + if (!this.requireAuth(() => this.showDifficultyDialog = true)) { + return; + } + + this.closeAllDialogs(); + this.showDifficultyDialog = true; + } + + closeDifficultyDialog(): void { + this.showDifficultyDialog = false; + this.errorMessage = ''; + } + + openOptionsDialog(): void { + this.closeAllDialogs(); + this.showOptionsDialog = true; + } + + closeOptionsDialog(): void { + this.showOptionsDialog = false; + this.errorMessage = ''; + } + + openJoinDialog(): void { + if (!this.requireAuth(() => this.showJoinDialog = true)) { + return; + } + + this.closeAllDialogs(); + this.showJoinDialog = true; + } + + closeJoinDialog(): void { + if (this.joiningGame) { + return; + } + this.showJoinDialog = false; + this.gameIdInput = ''; + this.errorMessage = ''; + } + + openImportDialog(): void { + if (!this.requireAuth(() => this.showImportDialog = true)) { + return; + } + + this.closeAllDialogs(); + this.showImportDialog = true; + } + + closeImportDialog(): void { + if (this.importing) { + return; + } + this.showImportDialog = false; + this.importText = ''; + this.importMode = 'fen'; + this.errorMessage = ''; + } + + setImportMode(mode: ImportMode): void { + this.importMode = mode; + this.errorMessage = ''; + } + startOneVsOne(): void { + if (!this.requireAuth(() => this.performStartOneVsOne())) { + return; + } + + this.performStartOneVsOne(); + } + + startVsBot(difficulty: Difficulty): void { + if (!this.requireAuth(() => this.performStartVsBot(difficulty))) { + return; + } + + this.performStartVsBot(difficulty); + } + + submitJoinGame(): void { + if (!this.requireAuth(() => this.performSubmitJoinGame())) { + return; + } + + this.performSubmitJoinGame(); + } + + submitImportGame(): void { + if (!this.requireAuth(() => this.performSubmitImportGame())) { + return; + } + + this.performSubmitImportGame(); + } + + onSpeechBubbleClick(): void { + this.showSpeechBubble = false; + this.isZoomedIn = true; + this.bubbleMessage = 'meow'; + this.showMeatEmoji = true; + this.showHappyBubble = false; + this.showSecondSpeechBubble = true; + + // Reset meat position + this.meatX = window.innerWidth / 2 - 100; + this.meatY = window.innerHeight / 2 + 150; + } + + onZoomedViewClick(): void { + this.isZoomedIn = false; + this.showSecondSpeechBubble = false; + this.showHappyBubble = false; + this.showMeatEmoji = false; + this.bubbleMessage = 'meow'; + + if (this.zoomTimeoutId) { + clearTimeout(this.zoomTimeoutId); + } + } + + onMeatMouseDown(event: MouseEvent): void { + this.isDraggingMeat = true; + const rect = (event.target as HTMLElement).getBoundingClientRect(); + this.meatDragOffsetX = event.clientX - rect.left; + this.meatDragOffsetY = event.clientY - rect.top; + } + + onMouseMove(event: MouseEvent): void { + if (!this.isDraggingMeat) { + return; + } + + this.meatX = event.clientX - this.meatDragOffsetX; + this.meatY = event.clientY - this.meatDragOffsetY; + + const gifElement = document.querySelector('.player-2-gif') as HTMLElement; + if (!gifElement) { + return; + } + + const gifRect = gifElement.getBoundingClientRect(); + const gifCenterX = gifRect.left + gifRect.width / 2; + const gifCenterY = gifRect.top + gifRect.height / 2; + + const meatElement = document.querySelector('.meat-emoji') as HTMLElement; + if (!meatElement) { + return; + } + + const meatRect = meatElement.getBoundingClientRect(); + const meatCenterX = meatRect.left + meatRect.width / 2; + const meatCenterY = meatRect.top + meatRect.height / 2; + + const distance = Math.sqrt( + Math.pow(meatCenterX - gifCenterX, 2) + Math.pow(meatCenterY - gifCenterY, 2) + ); + + if (distance < 50) { + this.onMeatFed(); + } + } + + onMouseUp(): void { + this.isDraggingMeat = false; + } + + onMeatFed(): void { + this.showMeatEmoji = false; + this.showSecondSpeechBubble = false; + this.showHappyBubble = true; + this.isDraggingMeat = false; + } + + private requireAuth(action: () => void): boolean { + if (this.authService.isLoggedIn()) { + return true; + } + + this.pendingAction = action; + this.authDialogService.openLogin(); + return false; + } + + private maybeRunPendingAction(): void { + if (!this.currentUser || this.authDialogState !== null || !this.pendingAction) { + return; + } + + const action = this.pendingAction; + this.pendingAction = null; + action(); + } + + private performStartOneVsOne(): void { if (this.creating) { return; } @@ -48,7 +349,9 @@ export class WelcomeComponent { .pipe(finalize(() => (this.creating = false))) .subscribe({ next: (game) => { - void this.router.navigate(['/game', game.gameId]); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to create a game.'); @@ -56,21 +359,23 @@ export class WelcomeComponent { }); } - startVsBot(difficulty: 'easy' | 'medium' | 'hard'): void { + private performStartVsBot(difficulty: Difficulty): void { if (this.creating) { return; } this.errorMessage = ''; this.creating = true; - this.showDifficultySelector = false; + this.showDifficultyDialog = false; this.gameApi .createGameVsBot(difficulty) .pipe(finalize(() => (this.creating = false))) .subscribe({ next: (game) => { - void this.router.navigate(['/game', game.gameId]); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to create a game against bot.'); @@ -78,21 +383,9 @@ export class WelcomeComponent { }); } - toggleDifficultySelector(): void { - this.showDifficultySelector = !this.showDifficultySelector; - this.showJoinGameForm = false; - this.errorMessage = ''; - } - - toggleJoinGameForm(): void { - this.showJoinGameForm = !this.showJoinGameForm; - this.showDifficultySelector = false; - this.errorMessage = ''; - this.gameIdInput = ''; - } - - joinGame(): void { - if (this.joiningGame || !this.gameIdInput.trim()) { + private performSubmitJoinGame(): void { + const gameId = this.gameIdInput.trim(); + if (this.joiningGame || !gameId) { return; } @@ -100,11 +393,14 @@ export class WelcomeComponent { this.joiningGame = true; this.gameApi - .getGame(this.gameIdInput.trim()) + .getGame(gameId) .pipe(finalize(() => (this.joiningGame = false))) .subscribe({ next: (game) => { - void this.router.navigate(['/game', game.gameId]); + this.closeJoinDialog(); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to find or join the game.'); @@ -112,26 +408,193 @@ export class WelcomeComponent { }); } - clearJoinGameForm(): void { - this.showJoinGameForm = false; - this.gameIdInput = ''; + private performSubmitImportGame(): void { + const trimmedImport = this.importText.trim(); + if (this.importing || !trimmedImport) { + return; + } + + this.errorMessage = ''; + this.importing = true; + + const importRequest = + this.importMode === 'fen' ? this.gameApi.importFen(trimmedImport) : this.gameApi.importPgn(trimmedImport); + + importRequest.pipe(finalize(() => (this.importing = false))).subscribe({ + next: (game) => { + this.closeImportDialog(); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); + }, + error: (error) => { + const defaultMessage = this.importMode === 'fen' ? 'Unable to import FEN.' : 'Unable to import PGN.'; + this.errorMessage = getErrorMessage(error, defaultMessage); + } + }); + } + + private closeAllDialogs(): void { + this.showDifficultyDialog = false; + this.showOptionsDialog = false; + this.showJoinDialog = false; + this.showImportDialog = false; this.errorMessage = ''; } - toggleDarkMode(): void { - const htmlElement = document.documentElement; - const isDarkMode = htmlElement.getAttribute('data-theme') === 'dark'; - - if (isDarkMode) { - htmlElement.removeAttribute('data-theme'); - localStorage.removeItem('theme'); - } else { - htmlElement.setAttribute('data-theme', 'dark'); - localStorage.setItem('theme', 'dark'); - } + private generateStars(count: number): void { + this.stars = Array.from({ length: count }, () => { + const size = Math.random() * 2 + 0.5; + return { + style: { + width: `${size}px`, + height: `${size}px`, + left: `${Math.random() * 100}%`, + top: `${Math.random() * 62}%`, + '--d': `${(Math.random() * 3 + 1.5).toFixed(1)}s`, + '--dl': `${-(Math.random() * 6).toFixed(1)}s` + } + }; + }); } - isDarkMode(): boolean { - return document.documentElement.getAttribute('data-theme') === 'dark'; + private generateBackgroundBuildings(): void { + const specs = [ + { l: '0%', w: '7%', h: '30vh' }, + { l: '3%', w: '4%', h: '18vh' }, // New building + { l: '7%', w: '5%', h: '22vh' }, + { l: '11%', w: '8%', h: '28vh' }, + { l: '15%', w: '6%', h: '20vh' }, + { l: '18.5%', w: '4%', h: '18vh' }, + { l: '22.5%', w: '6%', h: '26vh' }, + { l: '28%', w: '5%', h: '25vh' }, + { l: '32%', w: '4%', h: '15vh' }, + { l: '35.5%', w: '4.5%', h: '20vh' }, + { l: '42%', w: '5%', h: '28vh' }, + { l: '47%', w: '5%', h: '22vh' }, // New building + { l: '50%', w: '7%', h: '30vh' }, + { l: '55%', w: '6%', h: '27vh' }, + { l: '60.5%', w: '5%', h: '24vh' }, + { l: '64.5%', w: '3.5%', h: '17vh' }, + { l: '70%', w: '6%', h: '23vh' }, + { l: '75%', w: '4%', h: '19vh' }, + { l: '80.5%', w: '4%', h: '21vh' }, + { l: '85.5%', w: '9%', h: '32vh' }, + { l: '88%', w: '5%', h: '20vh' }, + { l: '91%', w: '3%', h: '16vh' }, // New building + { l: '94%', w: '6%', h: '27vh' } + ]; + + this.bgBuildings = specs.map((spec) => ({ + style: { left: spec.l, width: spec.w, height: spec.h } + })); + } + + private generateWindowsForAllBuildings(): void { + this.windows = { + wA1: this.generateWindows(3, 4, 0.6), + wA2: this.generateWindows(4, 5, 0.55), + wA3: this.generateWindows(5, 18, 0.5), + wB1: this.generateWindows(4, 3, 0.6), + wB2: this.generateWindows(5, 20, 0.55), + wC1: this.generateWindows(5, 3, 0.7), + wC2: this.generateWindows(6, 5, 0.65), + wC3: this.generateWindows(7, 24, 0.6), + wD1: this.generateWindows(6, 3, 0.6), + wD2: this.generateWindows(6, 20, 0.5), + wE1: this.generateWindows(3, 16, 0.45) + }; + } + + private generateWindows(cols: number, rows: number, litRate: number): WindowCell[] { + const total = cols * rows; + return Array.from({ length: total }, () => this.createWindowCell(litRate)); + } + + private createWindowCell(litRate: number): WindowCell { + const random = Math.random(); + let state: WindowCell['state'] = 'off'; + let color: string | undefined; + let glowColor: string | undefined; + + if (random < litRate * 0.58) { // Cool color + state = 'on'; + const coolIndex = Math.floor(Math.random() * this.coolColors.length); + color = this.coolColors[coolIndex]; + glowColor = this.coolGlowColors[coolIndex]; + } else if (random < litRate) { // Warm color + state = 'on'; + const warmIndex = Math.floor(Math.random() * this.warmColors.length); + color = this.warmColors[warmIndex]; + glowColor = this.warmGlowColors[warmIndex]; + } + + if (state === 'off') { + return { state, style: {} }; + } + + const baseDuration = (color && this.coolColors.includes(color)) ? 3 : 4; + return { + state, + color, + glowColor, + style: { + 'background-color': color || '', + 'box-shadow': glowColor ? `0 0 6px ${glowColor}, 0 0 16px ${glowColor}35` : '', + '--wd': `${(Math.random() * 4 + baseDuration).toFixed(1)}s`, + '--wdl': `${-(Math.random() * 8).toFixed(1)}s` + } + }; + } + + private startWindowFlicker(): void { + this.flickerIntervalId = setInterval(() => { + this.randomFlicker(); + }, 2800); + } + + private stopWindowFlicker(): void { + if (this.flickerIntervalId === undefined) { + return; + } + clearInterval(this.flickerIntervalId); + this.flickerIntervalId = undefined; + } + + private randomFlicker(): void { + const allWindows = Object.values(this.windows).flat(); + if (allWindows.length === 0) { + return; + } + + const pickCount = Math.floor(Math.random() * 6) + 1; + for (let i = 0; i < pickCount; i += 1) { + const target = allWindows[Math.floor(Math.random() * allWindows.length)]; + if (!target) { + continue; + } + + if (target.state === 'off') { + target.state = 'on'; + const isCool = Math.random() < 0.5; + const colors = isCool ? this.coolColors : this.warmColors; + const glowColors = isCool ? this.coolGlowColors : this.warmGlowColors; + const index = Math.floor(Math.random() * colors.length); + + target.color = colors[index]; + target.glowColor = glowColors[index]; + target.style = { + 'background-color': target.color || '', + 'box-shadow': target.glowColor ? `0 0 6px ${target.glowColor}, 0 0 16px ${target.glowColor}35` : '', + '--wd': `${(Math.random() * 4 + (isCool ? 3 : 4)).toFixed(1)}s`, + '--wdl': `${-(Math.random() * 8).toFixed(1)}s` + }; + } else { + target.state = 'off'; + target.color = undefined; + target.glowColor = undefined; + target.style = {}; + } + } } } diff --git a/src/app/services/auth-dialog.service.ts b/src/app/services/auth-dialog.service.ts new file mode 100644 index 0000000..774b4b6 --- /dev/null +++ b/src/app/services/auth-dialog.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export type AuthDialogState = 'login' | 'register' | null; + +@Injectable({ providedIn: 'root' }) +export class AuthDialogService { + private readonly dialogStateSubject = new BehaviorSubject(null); + + readonly dialogState$ = this.dialogStateSubject.asObservable(); + + openLogin(): void { + this.dialogStateSubject.next('login'); + } + + openRegister(): void { + this.dialogStateSubject.next('register'); + } + + close(): void { + this.dialogStateSubject.next(null); + } +} \ No newline at end of file diff --git a/src/app/services/auth.interceptor.ts b/src/app/services/auth.interceptor.ts new file mode 100644 index 0000000..c878833 --- /dev/null +++ b/src/app/services/auth.interceptor.ts @@ -0,0 +1,22 @@ +import { HttpInterceptorFn } from '@angular/common/http'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const token = localStorage.getItem('token'); + + // Add token to protected endpoints only (not registration or login) + const isProtectedEndpoint = + req.url.includes('/api/account/me') || + req.url.includes('/api/account/bots') || + req.url.includes('/api/account/official-bots') || + req.url.includes('/api/challenge'); + + if (token && isProtectedEndpoint) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + + return next(req); +}; diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..178b315 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,93 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { environment } from '../../environments/environment'; +import { LoginRequest, RegisterRequest, RegisterResponse, LoginResponse, CurrentUser } from '../models/auth.models'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly apiBase = environment.apiBaseUrl; + private readonly accountServiceUrl = environment.accountServiceUrl; + private readonly http = inject(HttpClient); + + private currentUserSubject = new BehaviorSubject(null); + public currentUser$ = this.currentUserSubject.asObservable(); + + constructor() { + this.loadCurrentUser(); + } + + login(username: string, password: string): Observable { + return this.http + .post(`${this.accountServiceUrl}/api/account/login`, { + username, + password + }) + .pipe( + tap((response) => { + localStorage.setItem('token', response.token); + localStorage.setItem('username', username); + // After login, fetch current user info + this.getCurrentUser().subscribe(); + }) + ); + } + + register(username: string, password: string, email?: string): Observable { + return this.http + .post(`${this.accountServiceUrl}/api/account`, { + username, + password, + email + }) + .pipe( + tap((response) => { + localStorage.setItem('username', response.username); + localStorage.setItem('userId', response.id); + this.currentUserSubject.next({ + id: response.id, + username: response.username, + rating: response.rating, + createdAt: response.createdAt + }); + }) + ); + } + + getCurrentUser(): Observable { + return this.http.get(`${this.accountServiceUrl}/api/account/me`).pipe( + tap((user) => { + localStorage.setItem('username', user.username); + localStorage.setItem('userId', user.id); + this.currentUserSubject.next(user); + }) + ); + } + + logout(): void { + localStorage.removeItem('token'); + localStorage.removeItem('username'); + localStorage.removeItem('userId'); + this.currentUserSubject.next(null); + } + + isLoggedIn(): boolean { + return !!localStorage.getItem('token'); + } + + private loadCurrentUser(): void { + const token = localStorage.getItem('token'); + const username = localStorage.getItem('username'); + const userId = localStorage.getItem('userId'); + if (token && username && userId) { + // Try to verify token is still valid by fetching current user + this.getCurrentUser().subscribe({ + error: () => { + // Token is invalid, clear it + this.logout(); + } + }); + } + } +} diff --git a/src/app/services/theme.service.ts b/src/app/services/theme.service.ts new file mode 100644 index 0000000..b450ae9 --- /dev/null +++ b/src/app/services/theme.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + private readonly darkModeSubject = new BehaviorSubject(false); + + readonly darkMode$ = this.darkModeSubject.asObservable(); + + initTheme(): void { + const savedTheme = localStorage.getItem('theme'); + this.applyDarkMode(savedTheme === 'dark'); + } + + toggleTheme(): void { + this.applyDarkMode(!this.darkModeSubject.value); + } + + setDarkMode(isDarkMode: boolean): void { + this.applyDarkMode(isDarkMode); + } + + private applyDarkMode(isDarkMode: boolean): void { + if (isDarkMode) { + document.documentElement.setAttribute('data-theme', 'dark'); + localStorage.setItem('theme', 'dark'); + } else { + document.documentElement.removeAttribute('data-theme'); + localStorage.removeItem('theme'); + } + + this.darkModeSubject.next(isDarkMode); + } +} \ No newline at end of file diff --git a/src/cityscape.html b/src/cityscape.html new file mode 100644 index 0000000..e69de29 diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index e55757c..9ad1e10 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,6 +1,7 @@ export const environment = { production: false, apiBaseUrl: '', + accountServiceUrl: '', wsBaseUrl: 'ws://localhost:8080', apiPath: '/api/board/game' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 42a2794..8c38733 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,6 +1,7 @@ export const environment = { production: true, apiBaseUrl: '', + accountServiceUrl: '', wsBaseUrl: 'ws://localhost:8080', apiPath: '/api/board/game' }; diff --git a/src/styles-variables.css b/src/styles-variables.css index 32b9dc3..8ff4919 100644 --- a/src/styles-variables.css +++ b/src/styles-variables.css @@ -1,6 +1,3 @@ -/* ======================================== - COLOR VARIABLES - Semantic Naming - ======================================== */ /* Light Mode Colors (Default) */ :root:not([data-theme='dark']) { @@ -22,11 +19,11 @@ --color-bg-input: #B9DAD1; --color-bg-input-focus: #B9C2DA; --color-bg-button: #C19EF5; - --color-bg-button-hover: #BA6D4B; + --color-bg-button-hover: #ba4ba7; --color-text-primary: #5A2C28; --color-text-button-hover: #F3C8A0; - --color-border: #5A2C28; + --color-border: #5a2843; } /* Dark Mode Colors */ @@ -122,4 +119,15 @@ SHADOWS ======================================== */ --shadow-md: 0 8px 24px rgba(90, 44, 40, 0.2); + /* Neon dialog / card variables (used by welcome dialogs, toolbar, login/register) */ + --bb-bg: rgba(8, 6, 28, 0.92); + --bb-border: #00d5ff; + --bb-glow: 0 0 18px rgba(0, 210, 255, 0.5), inset 0 0 10px rgba(0, 210, 255, 0.05); + --bb-tag: #00d5ff; + --bb-title: #d4f4ff; + --btn-bg: #00d5ff; + --btn-fg: #04000f; + --btn-glow: 0 0 14px rgba(0, 210, 255, 0.9); + --dlg-bg: rgba(8, 6, 28, 0.95); + --dlg-border: #00d5ff; } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index fa5a81b..520eacd 100644 --- a/src/styles.css +++ b/src/styles.css @@ -62,3 +62,254 @@ button, input { font: inherit; } + +.welcome-shell .import-game-form { + grid-column: 1 / -1; + background: var(--color-bg-card); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-lg-padding); + margin: var(--size-md) 0; + display: grid; + gap: var(--size-md); +} + +.welcome-shell .import-game-form p { + margin: 0; + font-weight: 600; + color: var(--color-text-primary); +} + +.welcome-shell .import-mode-group { + display: flex; + gap: var(--size-lg); + flex-wrap: wrap; +} + +.welcome-shell .import-mode-option { + display: inline-flex; + align-items: center; + gap: var(--size-sm); + font-weight: 600; + color: var(--color-text-primary); +} + +.welcome-shell .import-mode-option input { + accent-color: var(--color-primary); +} + +.welcome-shell .import-game-text { + width: 100%; + resize: vertical; + min-height: 110px; + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + background: var(--color-bg-input); + color: var(--color-text-primary); + padding: var(--size-md-padding); +} + +.welcome-shell .import-game-text:focus { + outline: none; + border-color: var(--color-secondary-mint); + box-shadow: 0 0 0 3px rgba(185, 218, 209, 0.2); +} + +.welcome-shell .theme-toggle-container { + position: absolute; + top: 20px; + right: 20px; + z-index: 100; +} + +.welcome-shell .switch { + display: inline-block; + position: relative; +} + +.welcome-shell .switch__input { + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + +.welcome-shell .switch__label { + position: relative; + display: inline-block; + width: 120px; + height: 60px; + background-color: #2B2B2B; + border: 5px solid #5B5B5B; + border-radius: 9999px; + cursor: pointer; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) translateX(-72%); + display: block; + width: 40px; + height: 40px; + background-color: #7B7B7B; + border-radius: 9999px; + box-shadow: 10px 0px 0 0 rgba(0, 0, 0, 0.2) inset; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__indicator::before, +.welcome-shell .switch__indicator::after { + position: absolute; + content: ''; + display: block; + background-color: #FFFFFF; + border-radius: 9999px; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__indicator::before { + top: 7px; + left: 7px; + width: 9px; + height: 9px; + opacity: 0.6; +} + +.welcome-shell .switch__indicator::after { + bottom: 8px; + right: 6px; + width: 14px; + height: 14px; + opacity: 0.8; +} + +.welcome-shell .switch__decoration { + position: absolute; + top: 65%; + left: 50%; + display: block; + width: 5px; + height: 5px; + background-color: #FFFFFF; + border-radius: 9999px; + animation: twinkle-stars 0.8s infinite -0.6s; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__decoration::before, +.welcome-shell .switch__decoration::after { + position: absolute; + display: block; + content: ''; + width: 5px; + height: 5px; + background-color: #FFFFFF; + border-radius: 9999px; +} + +.welcome-shell .switch__decoration::before { + top: -20px; + left: 10px; + opacity: 1; + animation: twinkle-stars 0.6s infinite; +} + +.welcome-shell .switch__decoration::after { + top: -7px; + left: 30px; + animation: twinkle-stars 0.6s infinite -0.2s; +} + +@keyframes twinkle-stars { + 50% { + opacity: 0.2; + } +} + +.welcome-shell .switch__input:checked + .switch__label { + background-color: #8FB5F5; + border-color: #347CF8; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__indicator { + background-color: #ECD21F; + box-shadow: none; + transform: translate(-50%, -50%) translateX(72%); +} + +.welcome-shell .switch__input:checked + .switch__label .switch__indicator::before, +.welcome-shell .switch__input:checked + .switch__label .switch__indicator::after { + display: none; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration { + top: 50%; + transform: translate(0%, -50%); + animation: cloud 8s linear infinite; + width: 20px; + height: 20px; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::before { + width: 10px; + height: 10px; + top: auto; + bottom: 0; + left: -8px; + animation: none; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::after { + width: 15px; + height: 15px; + top: auto; + bottom: 0; + left: 16px; + animation: none; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration, +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::before, +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::after { + border-radius: 9999px 9999px 0 0; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::after { + border-bottom-right-radius: 9999px; +} + +@keyframes cloud { + 0% { + transform: translate(0%, -50%); + } + 50% { + transform: translate(-50%, -50%); + } + 100% { + transform: translate(0%, -50%); + } +} + +@media (max-width: 768px) { + .welcome-shell .theme-toggle-container { + top: 10px; + right: 10px; + } + + .welcome-shell .switch__label { + width: 100px; + height: 50px; + } + + .welcome-shell .switch__indicator { + width: 33px; + height: 33px; + } +}