From e436dc871c1a415725345979d6f8d7862cdaeb0e Mon Sep 17 00:00:00 2001 From: "Lala, Shahd" Date: Thu, 14 May 2026 20:16:36 +0000 Subject: [PATCH] fix: console errors, notif error --- proxy.conf.json | 6 + .../challenge-create-dialog.component.html | 14 +- .../challenge-create-dialog.component.ts | 10 +- .../challenge-notification.component.ts | 2 +- .../login-dialog/login-dialog.component.html | 6 +- .../login-dialog/login-dialog.component.ts | 5 +- .../register-dialog.component.html | 11 +- .../register-dialog.component.ts | 5 +- .../components/toolbar/toolbar.component.css | 535 +++++++++++++++--- .../components/toolbar/toolbar.component.html | 206 ++++++- .../components/toolbar/toolbar.component.ts | 150 ++++- .../pages/challenges/challenges.component.ts | 2 +- src/app/services/challenge-event.service.ts | 14 + .../services/challenge-websocket.service.ts | 156 +++-- src/environments/environment.development.ts | 1 + src/environments/environment.staging.ts | 1 + src/environments/environment.ts | 1 + src/index.html | 3 + src/styles.css | 7 +- 19 files changed, 919 insertions(+), 216 deletions(-) diff --git a/proxy.conf.json b/proxy.conf.json index 1f2ef6a..fa98e8c 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -9,6 +9,12 @@ "secure": false, "changeOrigin": true }, + "/api/user/ws": { + "target": "http://localhost:8084", + "secure": false, + "changeOrigin": true, + "ws": true + }, "/api": { "target": "http://localhost:8080", "secure": false, diff --git a/src/app/components/challenge-create-dialog/challenge-create-dialog.component.html b/src/app/components/challenge-create-dialog/challenge-create-dialog.component.html index 0911de4..9536f0d 100644 --- a/src/app/components/challenge-create-dialog/challenge-create-dialog.component.html +++ b/src/app/components/challenge-create-dialog/challenge-create-dialog.component.html @@ -15,7 +15,7 @@
+ placeholder="Enter opponent's username" required /> Username is required @@ -24,7 +24,7 @@
- @@ -34,7 +34,7 @@
- @@ -58,13 +58,11 @@
- +
- +
@@ -72,7 +70,7 @@
- diff --git a/src/app/components/challenge-create-dialog/challenge-create-dialog.component.ts b/src/app/components/challenge-create-dialog/challenge-create-dialog.component.ts index 7c04003..2dc9a1f 100644 --- a/src/app/components/challenge-create-dialog/challenge-create-dialog.component.ts +++ b/src/app/components/challenge-create-dialog/challenge-create-dialog.component.ts @@ -124,11 +124,11 @@ export class ChallengeCreateDialogComponent implements OnInit, OnDestroy { this.errorMessage = ''; this.loading = true; + this.form.disable(); - const limitSeconds = Math.round((this.form.get('limitMinutes')?.value || 0) * 60); - const incrementSeconds = this.form.get('incrementSeconds')?.value || 0; - const ttlSeconds = this.form.get('ttlSeconds')?.value; - const color = (this.form.get('color')?.value || 'random') as PlayerColor; + const limitSeconds = Math.round((this.form.getRawValue().limitMinutes || 0) * 60); + const { incrementSeconds, ttlSeconds, color: rawColor } = this.form.getRawValue(); + const color = (rawColor || 'random') as PlayerColor; this.challengeService.sendChallenge(targetUsername, { timeControl: { @@ -138,7 +138,7 @@ export class ChallengeCreateDialogComponent implements OnInit, OnDestroy { color, ttlSeconds: ttlSeconds > 0 ? ttlSeconds : undefined }) - .pipe(finalize(() => (this.loading = false))) + .pipe(finalize(() => { this.loading = false; this.form.enable(); })) .subscribe({ next: (challenge) => { // Challenge sent successfully - navigate to challenges page to view status diff --git a/src/app/components/challenge-notification/challenge-notification.component.ts b/src/app/components/challenge-notification/challenge-notification.component.ts index 7c4130d..87d760f 100644 --- a/src/app/components/challenge-notification/challenge-notification.component.ts +++ b/src/app/components/challenge-notification/challenge-notification.component.ts @@ -59,7 +59,7 @@ export class ChallengeNotificationComponent { this.decliningChallenge = true; this.errorMessage = ''; - this.challengeService.declineChallenge(this.challenge.id, { reason: 'Not interested' }) + this.challengeService.declineChallenge(this.challenge.id, { reason: 'generic' }) .pipe(finalize(() => (this.decliningChallenge = false))) .subscribe({ next: () => { diff --git a/src/app/components/login-dialog/login-dialog.component.html b/src/app/components/login-dialog/login-dialog.component.html index 0f3d9e4..0be3592 100644 --- a/src/app/components/login-dialog/login-dialog.component.html +++ b/src/app/components/login-dialog/login-dialog.component.html @@ -4,15 +4,13 @@
- + @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 } diff --git a/src/app/components/login-dialog/login-dialog.component.ts b/src/app/components/login-dialog/login-dialog.component.ts index 25a74a2..dc025dd 100644 --- a/src/app/components/login-dialog/login-dialog.component.ts +++ b/src/app/components/login-dialog/login-dialog.component.ts @@ -38,15 +38,18 @@ export class LoginDialogComponent { this.isLoading = true; this.errorMessage = null; + this.loginForm.disable(); - const { username, password } = this.loginForm.value; + const { username, password } = this.loginForm.getRawValue(); this.authService.login(username, password).subscribe({ next: () => { this.isLoading = false; + this.loginForm.enable(); this.onSuccess.emit(); }, error: (err) => { this.isLoading = false; + this.loginForm.enable(); this.errorMessage = err.error?.message || 'Login failed. Please try again.'; } }); diff --git a/src/app/components/register-dialog/register-dialog.component.html b/src/app/components/register-dialog/register-dialog.component.html index 6562df8..4c75996 100644 --- a/src/app/components/register-dialog/register-dialog.component.html +++ b/src/app/components/register-dialog/register-dialog.component.html @@ -3,26 +3,23 @@
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 } + placeholder="Confirm Password" /> @if (errorMessage) {
{{ errorMessage }}
diff --git a/src/app/components/register-dialog/register-dialog.component.ts b/src/app/components/register-dialog/register-dialog.component.ts index ef11842..ae3e9cc 100644 --- a/src/app/components/register-dialog/register-dialog.component.ts +++ b/src/app/components/register-dialog/register-dialog.component.ts @@ -46,15 +46,18 @@ export class RegisterDialogComponent { this.isLoading = true; this.errorMessage = null; + this.registerForm.disable(); - const { username, email, password: pwd } = this.registerForm.value; + const { username, email, password: pwd } = this.registerForm.getRawValue(); this.authService.register(username, pwd, email).subscribe({ next: () => { this.isLoading = false; + this.registerForm.enable(); this.onSuccess.emit(); }, error: (err) => { this.isLoading = false; + this.registerForm.enable(); this.errorMessage = err.error?.message || 'Registration failed. Please try again.'; } diff --git a/src/app/components/toolbar/toolbar.component.css b/src/app/components/toolbar/toolbar.component.css index be8f759..b84e9fb 100644 --- a/src/app/components/toolbar/toolbar.component.css +++ b/src/app/components/toolbar/toolbar.component.css @@ -1,84 +1,487 @@ -@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; +/* ============ THEME TOKENS ============ */ +:host { + /* Light mode: warm sunset palette from background gradient */ + --nc-accent: #ff6b3d; + --nc-accent-hover: rgba(255, 107, 61, 0.15); + --nc-accent-badge: rgba(255, 107, 61, 0.9); + --nc-badge-text: #1a0800; + --nc-surface: rgba(26, 24, 56, 0.97); + --nc-nav-bg: linear-gradient(180deg, rgba(26,24,56,0.88) 0%, rgba(46,32,80,0.6) 70%, rgba(74,41,98,0) 100%); + --nc-text: #fff; + --nc-text-muted: rgba(255,255,255,0.7); + --nc-text-dim: rgba(255,255,255,0.45); + --nc-border: rgba(255,255,255,0.1); + --nc-popover-glow: 0 20px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,107,61,0.18); + --nc-unread-dot: #ff6b3d; + --nc-avatar-a: #d44d4a; + --nc-avatar-b: #8b3a6b; + --nc-danger: #ff7a7a; } -.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; +:host-context(html[data-theme='dark']) { + /* Dark mode: blue neon palette */ + --nc-accent: #00d5ff; + --nc-accent-hover: rgba(0, 213, 255, 0.12); + --nc-accent-badge: #00d5ff; + --nc-badge-text: #04000f; + --nc-surface: rgba(8, 6, 28, 0.97); + --nc-nav-bg: linear-gradient(180deg, rgba(8,5,20,0.88) 0%, rgba(8,5,20,0.58) 70%, rgba(8,5,20,0) 100%); + --nc-text: #fff; + --nc-text-muted: rgba(255,255,255,0.65); + --nc-text-dim: rgba(255,255,255,0.4); + --nc-border: rgba(255,255,255,0.08); + --nc-popover-glow: 0 20px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(0,213,255,0.15); + --nc-unread-dot: #00d5ff; + --nc-avatar-a: #00d5ff; + --nc-avatar-b: #1a5fa8; + --nc-danger: #ff7a7a; } -.gap-2 { - gap: 0.5rem; -} - -.user-section { +/* ============ NAV CONTAINER ============ */ +.nc-nav { + position: fixed; + top: 0; left: 0; right: 0; + height: 56px; + z-index: 100; + display: flex; align-items: center; + padding: 0 24px; + background: var(--nc-nav-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + font-family: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } -.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; +/* ============ LOGO ============ */ +.nc-logo { + display: flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; cursor: pointer; - transition: all 0.2s ease; - display: inline-flex; + user-select: none; +} + +.nc-logo-mark { + width: 24px; height: 24px; + background: var(--nc-accent); + display: flex; align-items: center; justify-content: center; - outline: none; + font-weight: 800; + color: var(--nc-badge-text); + font-size: 14px; +} + +.nc-logo-text { + font-size: 15px; + font-weight: 600; + letter-spacing: 0.03em; + color: var(--nc-text); +} + +/* ============ CENTER LINKS ============ */ +.nc-links { + flex: 1; + display: flex; + justify-content: center; + gap: 4px; +} + +.nc-link { + background: transparent; + border: none; + color: var(--nc-text-muted); + padding: 8px 14px; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.08em; + font-weight: 500; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + position: relative; + transition: color 0.15s; +} + +.nc-link:hover { color: var(--nc-text); } + +.nc-link::after { + content: ""; + position: absolute; + bottom: 2px; left: 14px; right: 14px; + height: 1px; + background: var(--nc-accent); + opacity: 0; + transition: opacity 0.15s; +} + +.nc-link:hover::after { opacity: 1; } + +/* ============ RIGHT CLUSTER ============ */ +.nc-right { + display: flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + margin-left: auto; +} + +/* ============ BELL ============ */ +.nc-bell { + width: 36px; height: 36px; + border: 1px solid var(--nc-border); + background: transparent; + color: var(--nc-text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: background 0.15s, color 0.15s; + font-family: inherit; +} + +.nc-bell:hover, +.nc-bell.is-open { + background: var(--nc-accent-hover); + color: var(--nc-text); +} + +/* ============ BADGE ============ */ +.nc-badge { + position: absolute; + top: 5px; right: 5px; + min-width: 14px; height: 14px; + border-radius: 7px; + background: var(--nc-accent-badge); + color: var(--nc-badge-text); + font-size: 9px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + padding: 0 3px; +} + +/* ============ PROFILE BUTTON ============ */ +.nc-profile { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 10px 4px 4px; + height: 36px; + border: 1px solid var(--nc-border); + background: transparent; + cursor: pointer; + color: var(--nc-text-muted); + font-family: inherit; + transition: background 0.15s, color 0.15s; +} + +.nc-profile:hover, +.nc-profile.is-open { + background: var(--nc-accent-hover); + color: var(--nc-text); +} + +.nc-profile-name { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; +} + +.nc-chevron { opacity: 0.5; } + +/* ============ AVATAR ============ */ +.nc-avatar { + border-radius: 50%; + background: linear-gradient(135deg, var(--nc-avatar-a) 0%, var(--nc-avatar-b) 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 700; + letter-spacing: 0.02em; + flex-shrink: 0; +} + +.nc-avatar-sm { width: 26px; height: 26px; font-size: 11px; } +.nc-avatar-md { width: 40px; height: 40px; font-size: 17px; } + +/* ============ DROPDOWN WRAPPER ============ */ +.nc-dropdown-wrap { position: relative; } + +/* ============ POPOVERS ============ */ +.nc-popover { + position: absolute; + top: calc(100% + 10px); + right: 0; + background: var(--nc-surface); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--nc-border); + box-shadow: var(--nc-popover-glow); + z-index: 200; + overflow: hidden; +} + +/* ============ NOTIFICATIONS PANEL ============ */ +.nc-notif { width: 360px; } + +.nc-notif-header { + padding: 14px 18px; + border-bottom: 1px solid rgba(255,255,255,0.06); + display: flex; + justify-content: space-between; + align-items: center; +} + +.nc-notif-header-title { + font-size: 11px; + letter-spacing: 0.22em; + color: var(--nc-text-muted); + text-transform: uppercase; + font-weight: 600; +} + +.nc-notif-list { max-height: 420px; overflow-y: auto; } + +.nc-notif-empty { + padding: 24px 18px; + text-align: center; + font-size: 13px; + color: var(--nc-text-dim); + letter-spacing: 0.04em; +} + +.nc-notif-row { + padding: 14px 18px; + border-bottom: 1px solid rgba(255,255,255,0.04); + position: relative; + display: flex; + gap: 12px; + align-items: flex-start; +} + +.nc-notif-row.is-unread { background: rgba(255,255,255,0.03); } + +.nc-notif-row.is-unread::before { + content: ""; + position: absolute; + left: 6px; top: 22px; + width: 4px; height: 4px; + border-radius: 50%; + background: var(--nc-unread-dot); +} + +.nc-notif-icon { + width: 32px; height: 32px; + flex-shrink: 0; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.12); + display: flex; + align-items: center; + justify-content: center; + color: var(--nc-accent); +} + +.nc-notif-body { flex: 1; min-width: 0; } + +.nc-notif-text { + font-size: 13px; + color: var(--nc-text); + line-height: 1.35; +} + +.nc-notif-text b { font-weight: 600; } + +.nc-notif-meta { + font-size: 10px; + color: var(--nc-text-dim); + margin-top: 4px; + letter-spacing: 0.08em; 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 { +.nc-notif-actions { display: flex; + gap: 6px; + margin-top: 10px; +} + +.nc-btn-accept, +.nc-btn-decline { + padding: 6px 12px; + font-size: 10px; + font-family: inherit; + letter-spacing: 0.18em; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 4px; + text-transform: uppercase; + font-weight: 600; + border: none; + transition: opacity 0.15s; +} + +.nc-btn-accept { + background: var(--nc-accent); + color: var(--nc-badge-text); + font-weight: 700; +} + +.nc-btn-decline { + background: transparent; + color: var(--nc-text-muted); + border: 1px solid rgba(255,255,255,0.15); +} + +.nc-btn-accept:disabled, +.nc-btn-decline:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.nc-notif-footer { + padding: 10px 18px; + border-top: 1px solid rgba(255,255,255,0.06); +} + +.nc-view-all { + width: 100%; + background: transparent; + border: none; + color: var(--nc-text-dim); + font-size: 11px; + font-family: inherit; + letter-spacing: 0.2em; + cursor: pointer; + text-transform: uppercase; + padding: 6px 0; + transition: color 0.15s; +} + +.nc-view-all:hover { color: var(--nc-text-muted); } + +/* ============ PROFILE MENU ============ */ +.nc-menu { width: 250px; } + +.nc-menu-header { + padding: 16px 16px 14px; + border-bottom: 1px solid rgba(255,255,255,0.06); + display: flex; + gap: 12px; align-items: center; } -.ms-auto { - margin-left: auto; +.nc-menu-user-name { + font-size: 14px; + color: var(--nc-text); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + +.nc-menu-user-sub { + font-size: 11px; + color: var(--nc-text-dim); + margin-top: 2px; + letter-spacing: 0.06em; +} + +.nc-menu-group { padding: 6px 0; } + +.nc-menu-group + .nc-menu-group { + border-top: 1px solid rgba(255,255,255,0.06); +} + +.nc-menu-item { + padding: 9px 16px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + color: var(--nc-text-muted); + font-size: 13px; + font-family: inherit; + background: transparent; + border: none; + width: 100%; + text-align: left; + transition: background 0.12s, color 0.12s; +} + +.nc-menu-item:hover { + background: var(--nc-accent-hover); + color: var(--nc-accent); +} + +.nc-menu-item.danger { color: var(--nc-danger); } +.nc-menu-item.danger:hover { background: rgba(255,122,122,0.08); color: var(--nc-danger); } + +.nc-menu-icon { opacity: 0.85; display: inline-flex; } +.nc-menu-label { flex: 1; } + +/* ============ DARK MODE TOGGLE PILL ============ */ +.nc-toggle { + width: 28px; height: 16px; + border-radius: 8px; + background: rgba(255,255,255,0.15); + position: relative; + flex-shrink: 0; + transition: background 0.2s; +} + +.nc-toggle.is-on { background: var(--nc-accent); } + +.nc-toggle::after { + content: ""; + position: absolute; + top: 2px; left: 2px; + width: 12px; height: 12px; + border-radius: 50%; + background: #fff; + transition: left 0.2s; +} + +.nc-toggle.is-on::after { left: 14px; } + +/* ============ AUTH BUTTONS (logged out) ============ */ +.nc-auth-btn { + background: transparent; + border: 1px solid var(--nc-border); + color: var(--nc-text-muted); + padding: 7px 14px; + font-size: 11px; + font-family: inherit; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.nc-auth-btn:hover { + background: rgba(255,255,255,0.06); + color: var(--nc-text); +} + +.nc-auth-btn--primary { + background: var(--nc-accent); + border-color: var(--nc-accent); + color: var(--nc-badge-text); +} + +.nc-auth-btn--primary:hover { + filter: brightness(1.1); + color: var(--nc-badge-text); +} + +/* ============ NOTIF SCROLLBAR ============ */ +.nc-notif-list::-webkit-scrollbar { width: 6px; } +.nc-notif-list::-webkit-scrollbar-track { background: transparent; } +.nc-notif-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } diff --git a/src/app/components/toolbar/toolbar.component.html b/src/app/components/toolbar/toolbar.component.html index cba57a3..5526099 100644 --- a/src/app/components/toolbar/toolbar.component.html +++ b/src/app/components/toolbar/toolbar.component.html @@ -1,28 +1,188 @@ -