feat: NCS-63 User account implementation #2

Merged
shosho996 merged 26 commits from feat/NCS-66 into main 2026-05-06 10:51:30 +02:00
13 changed files with 447 additions and 56 deletions
Showing only changes of commit a3255602b3 - Show all commits
+2
View File
@@ -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: '' }
];
+30 -1
View File
@@ -1,4 +1,4 @@
/* Shared Button Template */
/* Shared Button Template - All Button Styles Consolidated */
.app-btn {
background: var(--btn-bg);
@@ -38,3 +38,32 @@
.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;
}
@@ -1,3 +1,5 @@
@import '../../button-template.css';
.input-card {
background: var(--color-bg-card);
border: var(--border-width) solid var(--color-border);
@@ -40,8 +42,6 @@
}
@import '../../button-template.css';
.hint-text {
margin: 0;
color: var(--color-text-primary);
@@ -1,3 +1,5 @@
@import '../../button-template.css';
.dialog-overlay {
position: fixed;
inset: 0;
@@ -27,19 +29,6 @@
text-align: center;
}
@import '../../button-template.css';
.dialog-actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.dialog-actions .app-btn {
flex: 1;
min-width: 120px;
}
.dialog-input {
width: 100%;
background: rgba(4, 4, 20, 0.62);
@@ -1,3 +1,5 @@
@import '../../button-template.css';
.promotion-dialog-overlay {
position: fixed;
top: 0;
@@ -73,21 +75,3 @@
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@import '../../button-template.css';
.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;
}
@@ -1,3 +1,5 @@
@import '../../button-template.css';
.dialog-overlay {
position: fixed;
inset: 0;
@@ -27,19 +29,6 @@
text-align: center;
}
@import '../../button-template.css';
.dialog-actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.dialog-actions .app-btn {
flex: 1;
min-width: 120px;
}
.dialog-input {
width: 100%;
background: rgba(4, 4, 20, 0.62);
@@ -1,9 +1,12 @@
@import '../../button-template.css';
.navbar {
background: var(--dlg-bg);
border: 1.5px solid var(--dlg-border);
box-shadow: var(--bb-glow);
border-radius: 4px;
padding: 0.25rem 0.75rem;
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 {
@@ -12,10 +15,70 @@
color: var(--bb-title) !important;
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 1px;
cursor: pointer;
}
.gap-2 {
gap: 0.5rem;
}
@import '../../button-template.css';
.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;
}
@@ -7,8 +7,10 @@
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
</button>
@if (currentUser; as user) {
<div class="d-flex align-items-center gap-2">
<span style="color:var(--bb-title);font-weight:700">{{ user.username }}</span>
<div class="d-flex align-items-center gap-2 user-section">
<button type="button" class="me-btn" (click)="goToProfile()">
👤 {{ user.username }}
</button>
<button type="button" class="app-btn" (click)="logout()">Logout</button>
</div>
} @else {
@@ -1,5 +1,6 @@
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';
@@ -20,6 +21,7 @@ export class ToolbarComponent implements OnInit {
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;
@@ -71,6 +73,10 @@ export class ToolbarComponent implements OnInit {
this.themeService.toggleTheme();
}
goToProfile(): void {
this.router.navigate(['/profile']);
}
onLoginSuccess(): void {
this.closeLoginDialog();
}
+154
View File
@@ -0,0 +1,154 @@
@import '../welcome/welcome.component.css';
.profile-card-container {
position: absolute;
bottom: 50%;
left: 50%;
transform: translate(-50%, 50%);
width: min(500px, 90vw);
background: var(--dlg-bg);
border: 1.5px solid var(--dlg-border);
box-shadow: var(--bb-glow);
border-radius: 4px;
padding: 2rem;
z-index: 15;
}
.profile-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--dlg-border);
padding-bottom: 1.5rem;
}
.back-btn {
align-self: flex-start;
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);
}
.profile-header h1 {
margin: 0;
font-family: 'Bebas Neue', sans-serif;
font-size: 28px;
letter-spacing: 2px;
color: var(--bb-title);
text-align: center;
width: 100%;
}
.profile-content {
display: grid;
gap: 2rem;
}
.player-avatar-section {
display: flex;
justify-content: center;
}
.avatar-placeholder {
width: 100px;
height: 100px;
border: 2px solid var(--dlg-border);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 210, 255, 0.08);
font-size: 3.5rem;
}
.player-info-section {
display: grid;
gap: 1.2rem;
}
.info-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 1rem;
align-items: center;
padding-bottom: 0.8rem;
border-bottom: 1px solid rgba(0, 210, 255, 0.2);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: var(--bb-tag);
font-family: 'Space Mono', monospace;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.info-value {
color: var(--bb-title);
font-family: 'Space Mono', monospace;
font-size: 13px;
word-break: break-all;
}
.sunset .profile-card-container {
background: var(--dlg-bg);
border-color: var(--dlg-border);
}
.sunset .back-btn {
border-color: var(--dlg-border);
color: var(--bb-title);
}
.sunset .back-btn:hover {
background: rgba(242, 106, 226, 0.1);
border-color: var(--bb-tag);
color: var(--bb-tag);
}
@media (max-width: 600px) {
.profile-card-container {
padding: 1.5rem;
width: 95vw;
}
.info-row {
grid-template-columns: 1fr;
gap: 0.3rem;
}
.info-label {
font-size: 10px;
}
.info-value {
font-size: 12px;
}
.avatar-placeholder {
width: 80px;
height: 80px;
font-size: 2.5rem;
}
}
@@ -0,0 +1,66 @@
<div class="cityscape-shell" [class.sunset]="isSunsetMode">
<div class="scene">
<div class="sky">
<div class="stars-layer">
<div class="star" *ngFor="let star of stars" [ngStyle]="star.style"></div>
</div>
<div class="sun"></div>
<div class="cloud-wrap">
<div class="cloud cloud-a"></div>
<div class="cloud cloud-b"></div>
<div class="cloud cloud-c"></div>
<div class="cloud cloud-d"></div>
<div class="cloud cloud-e"></div>
</div>
</div>
<div class="bg-layer">
<div class="bg-bldg" *ngFor="let building of bgBuildings" [ngStyle]="building.style"></div>
</div>
<div class="main-layer">
<div class="profile-card-container">
<div class="profile-header">
<button type="button" class="back-btn" (click)="goBack()">← BACK</button>
<h1>MY PROFILE</h1>
</div>
@if (currentUser; as user) {
<div class="profile-content">
<div class="player-avatar-section">
<div class="avatar-placeholder">
<span>👤</span>
</div>
</div>
<div class="player-info-section">
<div class="info-row">
<span class="info-label">Player ID:</span>
<span class="info-value">{{ user.id }}</span>
</div>
<div class="info-row">
<span class="info-label">Username:</span>
<span class="info-value">{{ user.username }}</span>
</div>
<div class="info-row">
<span class="info-label">Rating:</span>
<span class="info-value">{{ user.rating }}</span>
</div>
<div class="info-row">
<span class="info-label">Member Since:</span>
<span class="info-value">{{ user.createdAt | date: 'MMM dd, yyyy' }}</span>
</div>
</div>
</div>
}
</div>
</div>
<div class="haze"></div>
<div class="ground"></div>
</div>
</div>
+107
View File
@@ -0,0 +1,107 @@
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<string, string>;
}
interface BackgroundBuilding {
style: Record<string, string>;
}
@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;
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(['']);
}
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 }
}));
}
}
+1 -1
View File
@@ -94,7 +94,7 @@
<div class="bb-tag">WELCOME</div>
<div class="bb-title" style="font-size:clamp(16px,1.8vw,26px);">WELCOME TO<br />NOWCHESS</div>
<div class="bb-subtitle">Play your next move from the skyline.</div>
<button type="button" class="app-btn" style="padding:7px 20px;font-size:11px;" (click)="startOneVsOne()"
<button type="button" class="app-btn" (click)="startOneVsOne()"
[disabled]="creating">
{{ creating ? 'CREATING...' : 'START NOW →' }}
</button>