-
-
-
- {{ errorMessage }}
-
-
-
-
-
-
Incoming Challenges
-
Loading...
-
-
-
No incoming challenges
-
-
-
0" class="challenge-list">
-
-
-
-
-
- Status:
-
- {{ challenge.status | uppercase }}
-
-
-
- Expires in:
- {{ getExpirationInfo(challenge) }}
-
-
-
-
-
-
-
-
-
+
-
-
-
Outgoing Challenges
-
-
-
No outgoing challenges
-
-
-
0" class="challenge-list">
-
-
-
-
-
- Status:
-
- {{ challenge.status | uppercase }}
-
-
-
- Expires in:
- {{ getExpirationInfo(challenge) }}
-
-
-
-
-
-
-
-
+
+ {{ errorMessage }}
-
-
+
+
+
+
+
Incoming Challenges
+
Loading...
+
+
+
No incoming challenges
+
+
+
0" class="challenge-list">
+
+
+
+
+
+ Status:
+
+ {{ challenge.status | uppercase }}
+
+
+
+ Expires in:
+ {{ getExpirationInfo(challenge) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Outgoing Challenges
+
+
+
No outgoing challenges
+
+
+
0" class="challenge-list">
+
+
+
+
+
+ Status:
+
+ {{ challenge.status | uppercase }}
+
+
+
+ Expires in:
+ {{ getExpirationInfo(challenge) }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/challenges/challenges.component.ts b/src/app/pages/challenges/challenges.component.ts
index ebca54e..4db5b13 100644
--- a/src/app/pages/challenges/challenges.component.ts
+++ b/src/app/pages/challenges/challenges.component.ts
@@ -8,160 +8,160 @@ import { Challenge } from '../../models/challenge.models';
import { getErrorMessage } from '../../core/http/error-message.util';
@Component({
- selector: 'app-challenges',
- standalone: true,
- imports: [CommonModule],
- templateUrl: './challenges.component.html',
- styleUrls: ['./challenges.component.css']
+ selector: 'app-challenges',
+ standalone: true,
+ imports: [CommonModule],
+ templateUrl: './challenges.component.html',
+ styleUrls: ['./challenges.component.css']
})
export class ChallengesComponent implements OnInit, OnDestroy {
- private readonly challengeService = inject(ChallengeService);
- private readonly challengeEventService = inject(ChallengeEventService);
- private readonly router = inject(Router);
- private readonly destroyRef = inject(DestroyRef);
+ private readonly challengeService = inject(ChallengeService);
+ private readonly challengeEventService = inject(ChallengeEventService);
+ private readonly router = inject(Router);
+ private readonly destroyRef = inject(DestroyRef);
- incomingChallenges: Challenge[] = [];
- outgoingChallenges: Challenge[] = [];
- loading = false;
- errorMessage = '';
+ incomingChallenges: Challenge[] = [];
+ outgoingChallenges: Challenge[] = [];
+ loading = false;
+ errorMessage = '';
- private pollInterval: any = null;
- private readonly pollIntervalMs = 5000; // Poll every 5 seconds
+ private pollInterval: any = null;
+ private readonly pollIntervalMs = 5000; // Poll every 5 seconds
- ngOnInit(): void {
- this.loadChallenges(true);
+ ngOnInit(): void {
+ this.loadChallenges(true);
- // Subscribe to challenge events
- this.challengeEventService.getChallengeReceived$()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe(() => {
- this.loadChallenges();
- });
+ // Subscribe to challenge events
+ this.challengeEventService.getChallengeReceived$()
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => {
+ this.loadChallenges();
+ });
- // Start polling for challenge updates
- this.startPolling();
- }
-
- ngOnDestroy(): void {
- this.stopPolling();
- }
-
- private startPolling(): void {
- this.pollInterval = setInterval(() => {
- this.loadChallenges(false);
- }, this.pollIntervalMs);
- }
-
- private stopPolling(): void {
- if (this.pollInterval) {
- clearInterval(this.pollInterval);
- this.pollInterval = null;
- }
- }
-
- loadChallenges(showLoader = false): void {
- if (showLoader) {
- this.loading = true;
- this.errorMessage = '';
+ // Start polling for challenge updates
+ this.startPolling();
}
- this.challengeService.listChallenges()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe({
- next: (response) => {
- this.incomingChallenges = response.in || response.incoming || [];
- this.outgoingChallenges = response.out || response.outgoing || [];
- if (showLoader) {
- this.loading = false;
- }
- },
- error: (error) => {
- this.errorMessage = getErrorMessage(error, 'Failed to load challenges');
- if (showLoader) {
- this.loading = false;
- }
+ ngOnDestroy(): void {
+ this.stopPolling();
+ }
+
+ private startPolling(): void {
+ this.pollInterval = setInterval(() => {
+ this.loadChallenges(false);
+ }, this.pollIntervalMs);
+ }
+
+ private stopPolling(): void {
+ if (this.pollInterval) {
+ clearInterval(this.pollInterval);
+ this.pollInterval = null;
}
- });
- }
-
- acceptChallenge(challenge: Challenge): void {
- this.challengeService.acceptChallenge(challenge.id)
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe({
- next: () => {
- this.challengeEventService.onChallengeAccepted(challenge);
- this.loadChallenges();
- // Navigate to game (if backend creates game automatically)
- },
- error: (error) => {
- this.errorMessage = getErrorMessage(error, 'Failed to accept challenge');
- }
- });
- }
-
- declineChallenge(challenge: Challenge): void {
- this.challengeService.declineChallenge(challenge.id, { reason: 'Not interested' })
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe({
- next: () => {
- this.challengeEventService.removeChallenge(challenge.id);
- this.loadChallenges();
- },
- error: (error) => {
- this.errorMessage = getErrorMessage(error, 'Failed to decline challenge');
- }
- });
- }
-
- cancelChallenge(challenge: Challenge): void {
- this.challengeService.cancelChallenge(challenge.id)
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe({
- next: () => {
- this.loadChallenges();
- },
- error: (error) => {
- this.errorMessage = getErrorMessage(error, 'Failed to cancel challenge');
- }
- });
- }
-
- goBack(): void {
- void this.router.navigate(['/']);
- }
-
- getTimeControlDisplay(challenge: Challenge): string {
- const { limit, increment } = challenge.timeControl;
- if (!limit || !increment) {
- return 'Unlimited';
- }
- const minutes = Math.floor(limit / 60);
- return `${minutes}+${increment}`;
- }
-
- getChallengerDisplay(challenge: Challenge): string {
- return challenge.challenger.name;
- }
-
- getOpponentDisplay(challenge: Challenge): string {
- return challenge.destUser.name;
- }
-
- getExpirationInfo(challenge: Challenge): string {
- const expiresAt = new Date(challenge.expiresAt);
- const now = new Date();
- const diffMs = expiresAt.getTime() - now.getTime();
-
- if (diffMs <= 0 || challenge.status === 'expired') {
- return 'Expired';
}
- const minutes = Math.floor(diffMs / 60000);
- if (minutes > 60) {
- const hours = Math.floor(minutes / 60);
- return `${hours}h`;
+ loadChallenges(showLoader = false): void {
+ if (showLoader) {
+ this.loading = true;
+ this.errorMessage = '';
+ }
+
+ this.challengeService.listChallenges()
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: (response) => {
+ this.incomingChallenges = response.in || response.incoming || [];
+ this.outgoingChallenges = response.out || response.outgoing || [];
+ if (showLoader) {
+ this.loading = false;
+ }
+ },
+ error: (error) => {
+ this.errorMessage = getErrorMessage(error, 'Failed to load challenges');
+ if (showLoader) {
+ this.loading = false;
+ }
+ }
+ });
}
- return `${minutes}m`;
- }
+ acceptChallenge(challenge: Challenge): void {
+ this.challengeService.acceptChallenge(challenge.id)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: () => {
+ this.challengeEventService.onChallengeAccepted(challenge);
+ this.loadChallenges();
+ // Navigate to game (if backend creates game automatically)
+ },
+ error: (error) => {
+ this.errorMessage = getErrorMessage(error, 'Failed to accept challenge');
+ }
+ });
+ }
+
+ declineChallenge(challenge: Challenge): void {
+ this.challengeService.declineChallenge(challenge.id, { reason: 'Not interested' })
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: () => {
+ this.challengeEventService.removeChallenge(challenge.id);
+ this.loadChallenges();
+ },
+ error: (error) => {
+ this.errorMessage = getErrorMessage(error, 'Failed to decline challenge');
+ }
+ });
+ }
+
+ cancelChallenge(challenge: Challenge): void {
+ this.challengeService.cancelChallenge(challenge.id)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: () => {
+ this.loadChallenges();
+ },
+ error: (error) => {
+ this.errorMessage = getErrorMessage(error, 'Failed to cancel challenge');
+ }
+ });
+ }
+
+ goBack(): void {
+ void this.router.navigate(['/']);
+ }
+
+ getTimeControlDisplay(challenge: Challenge): string {
+ const { limit, increment } = challenge.timeControl;
+ if (!limit || !increment) {
+ return 'Unlimited';
+ }
+ const minutes = Math.floor(limit / 60);
+ return `${minutes}+${increment}`;
+ }
+
+ getChallengerDisplay(challenge: Challenge): string {
+ return challenge.challenger.name;
+ }
+
+ getOpponentDisplay(challenge: Challenge): string {
+ return challenge.destUser.name;
+ }
+
+ getExpirationInfo(challenge: Challenge): string {
+ const expiresAt = new Date(challenge.expiresAt);
+ const now = new Date();
+ const diffMs = expiresAt.getTime() - now.getTime();
+
+ if (diffMs <= 0 || challenge.status === 'expired') {
+ return 'Expired';
+ }
+
+ const minutes = Math.floor(diffMs / 60000);
+ if (minutes > 60) {
+ const hours = Math.floor(minutes / 60);
+ return `${hours}h`;
+ }
+
+ return `${minutes}m`;
+ }
}
diff --git a/src/app/services/challenge-event.service.ts b/src/app/services/challenge-event.service.ts
index fa55170..fca0c7d 100644
--- a/src/app/services/challenge-event.service.ts
+++ b/src/app/services/challenge-event.service.ts
@@ -8,57 +8,57 @@ import { Challenge } from '../models/challenge.models';
*/
@Injectable({ providedIn: 'root' })
export class ChallengeEventService {
- private readonly incomingChallenges$ = new BehaviorSubject
([]);
- private readonly challengeReceived$ = new Subject();
- private readonly challengeAccepted$ = new Subject();
- private readonly challengeDeclined$ = new Subject();
+ private readonly incomingChallenges$ = new BehaviorSubject([]);
+ private readonly challengeReceived$ = new Subject();
+ private readonly challengeAccepted$ = new Subject();
+ private readonly challengeDeclined$ = new Subject();
- getIncomingChallenges$(): Observable {
- return this.incomingChallenges$.asObservable();
- }
+ getIncomingChallenges$(): Observable {
+ return this.incomingChallenges$.asObservable();
+ }
- getChallengeReceived$(): Observable {
- return this.challengeReceived$.asObservable();
- }
+ getChallengeReceived$(): Observable {
+ return this.challengeReceived$.asObservable();
+ }
- getChallengeAccepted$(): Observable {
- return this.challengeAccepted$.asObservable();
- }
+ getChallengeAccepted$(): Observable {
+ return this.challengeAccepted$.asObservable();
+ }
- getChallengeDeclined$(): Observable {
- return this.challengeDeclined$.asObservable();
- }
+ getChallengeDeclined$(): Observable {
+ return this.challengeDeclined$.asObservable();
+ }
- /**
- * Called when a new challenge is received via WebSocket
- */
- onChallengeReceived(challenge: Challenge): void {
- const current = this.incomingChallenges$.value;
- this.incomingChallenges$.next([...current, challenge]);
- this.challengeReceived$.next(challenge);
- }
+ /**
+ * Called when a new challenge is received via WebSocket
+ */
+ onChallengeReceived(challenge: Challenge): void {
+ const current = this.incomingChallenges$.value;
+ this.incomingChallenges$.next([...current, challenge]);
+ this.challengeReceived$.next(challenge);
+ }
- /**
- * Called when a challenge is accepted
- */
- onChallengeAccepted(challenge: Challenge): void {
- const current = this.incomingChallenges$.value;
- this.incomingChallenges$.next(current.filter(c => c.id !== challenge.id));
- this.challengeAccepted$.next(challenge);
- }
+ /**
+ * Called when a challenge is accepted
+ */
+ onChallengeAccepted(challenge: Challenge): void {
+ const current = this.incomingChallenges$.value;
+ this.incomingChallenges$.next(current.filter(c => c.id !== challenge.id));
+ this.challengeAccepted$.next(challenge);
+ }
- /**
- * Called when a challenge is declined or expires
- */
- onChallengeRemoved(challengeId: string): void {
- const current = this.incomingChallenges$.value;
- this.incomingChallenges$.next(current.filter(c => c.id !== challengeId));
- }
+ /**
+ * Called when a challenge is declined or expires
+ */
+ onChallengeRemoved(challengeId: string): void {
+ const current = this.incomingChallenges$.value;
+ this.incomingChallenges$.next(current.filter(c => c.id !== challengeId));
+ }
- /**
- * Remove a challenge from the incoming list
- */
- removeChallenge(challengeId: string): void {
- this.onChallengeRemoved(challengeId);
- }
+ /**
+ * Remove a challenge from the incoming list
+ */
+ removeChallenge(challengeId: string): void {
+ this.onChallengeRemoved(challengeId);
+ }
}
diff --git a/src/app/services/challenge-websocket.service.ts b/src/app/services/challenge-websocket.service.ts
index 18eb554..d13131f 100644
--- a/src/app/services/challenge-websocket.service.ts
+++ b/src/app/services/challenge-websocket.service.ts
@@ -9,127 +9,127 @@ import { Challenge } from '../models/challenge.models';
*/
@Injectable({ providedIn: 'root' })
export class ChallengeWebSocketService {
- private readonly challengeEventService = inject(ChallengeEventService);
- private ws: WebSocket | null = null;
- private reconnectAttempts = 0;
- private readonly maxReconnectAttempts = 5;
- private readonly reconnectDelay = 3000;
+ private readonly challengeEventService = inject(ChallengeEventService);
+ private ws: WebSocket | null = null;
+ private reconnectAttempts = 0;
+ private readonly maxReconnectAttempts = 5;
+ private readonly reconnectDelay = 3000;
- /**
- * Initialize WebSocket connection for challenge events
- */
- connect(): void {
- if (this.ws) {
- return; // Already connected
+ /**
+ * Initialize WebSocket connection for challenge events
+ */
+ connect(): void {
+ if (this.ws) {
+ return; // Already connected
+ }
+
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
+ const wsUrl = `${wsProtocol}://${window.location.host}/ws/challenges`;
+
+ try {
+ this.ws = new WebSocket(wsUrl);
+
+ this.ws.onopen = () => {
+ console.log('Challenge WebSocket connected');
+ this.reconnectAttempts = 0;
+ };
+
+ this.ws.onmessage = (event) => {
+ this.handleMessage(event.data);
+ };
+
+ this.ws.onerror = (error) => {
+ console.error('Challenge WebSocket error:', error);
+ };
+
+ this.ws.onclose = () => {
+ console.log('Challenge WebSocket disconnected');
+ this.ws = null;
+ this.attemptReconnect();
+ };
+ } catch (error) {
+ console.error('Failed to create WebSocket:', error);
+ this.attemptReconnect();
+ }
}
- const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
- const wsUrl = `${wsProtocol}://${window.location.host}/ws/challenges`;
-
- try {
- this.ws = new WebSocket(wsUrl);
-
- this.ws.onopen = () => {
- console.log('Challenge WebSocket connected');
- this.reconnectAttempts = 0;
- };
-
- this.ws.onmessage = (event) => {
- this.handleMessage(event.data);
- };
-
- this.ws.onerror = (error) => {
- console.error('Challenge WebSocket error:', error);
- };
-
- this.ws.onclose = () => {
- console.log('Challenge WebSocket disconnected');
- this.ws = null;
- this.attemptReconnect();
- };
- } catch (error) {
- console.error('Failed to create WebSocket:', error);
- this.attemptReconnect();
- }
- }
-
- /**
- * Close the WebSocket connection
- */
- disconnect(): void {
- if (this.ws) {
- this.ws.close();
- this.ws = null;
- }
- }
-
- /**
- * Send a message through WebSocket
- */
- send(message: any): void {
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
- this.ws.send(JSON.stringify(message));
- }
- }
-
- /**
- * Handle incoming WebSocket messages
- */
- private handleMessage(data: string): void {
- try {
- const message = JSON.parse(data);
-
- if (!message.type) {
- return;
- }
-
- switch (message.type) {
- case 'challenge.received':
- if (message.challenge) {
- this.challengeEventService.onChallengeReceived(message.challenge as Challenge);
- }
- break;
-
- case 'challenge.accepted':
- if (message.challenge) {
- this.challengeEventService.onChallengeAccepted(message.challenge as Challenge);
- }
- break;
-
- case 'challenge.declined':
- if (message.challengeId) {
- this.challengeEventService.removeChallenge(message.challengeId);
- }
- break;
-
- case 'challenge.expired':
- if (message.challengeId) {
- this.challengeEventService.removeChallenge(message.challengeId);
- }
- break;
-
- default:
- console.debug('Unknown challenge message type:', message.type);
- }
- } catch (error) {
- console.error('Failed to parse WebSocket message:', error);
- }
- }
-
- /**
- * Attempt to reconnect to WebSocket
- */
- private attemptReconnect(): void {
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
- console.error('Max WebSocket reconnection attempts reached');
- return;
+ /**
+ * Close the WebSocket connection
+ */
+ disconnect(): void {
+ if (this.ws) {
+ this.ws.close();
+ this.ws = null;
+ }
}
- this.reconnectAttempts++;
- console.log(`Attempting WebSocket reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
+ /**
+ * Send a message through WebSocket
+ */
+ send(message: any): void {
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+ this.ws.send(JSON.stringify(message));
+ }
+ }
- setTimeout(() => {
- this.connect();
- }, this.reconnectDelay);
- }
+ /**
+ * Handle incoming WebSocket messages
+ */
+ private handleMessage(data: string): void {
+ try {
+ const message = JSON.parse(data);
+
+ if (!message.type) {
+ return;
+ }
+
+ switch (message.type) {
+ case 'challenge.received':
+ if (message.challenge) {
+ this.challengeEventService.onChallengeReceived(message.challenge as Challenge);
+ }
+ break;
+
+ case 'challenge.accepted':
+ if (message.challenge) {
+ this.challengeEventService.onChallengeAccepted(message.challenge as Challenge);
+ }
+ break;
+
+ case 'challenge.declined':
+ if (message.challengeId) {
+ this.challengeEventService.removeChallenge(message.challengeId);
+ }
+ break;
+
+ case 'challenge.expired':
+ if (message.challengeId) {
+ this.challengeEventService.removeChallenge(message.challengeId);
+ }
+ break;
+
+ default:
+ console.debug('Unknown challenge message type:', message.type);
+ }
+ } catch (error) {
+ console.error('Failed to parse WebSocket message:', error);
+ }
+ }
+
+ /**
+ * Attempt to reconnect to WebSocket
+ */
+ private attemptReconnect(): void {
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.error('Max WebSocket reconnection attempts reached');
+ return;
+ }
+
+ this.reconnectAttempts++;
+ console.log(`Attempting WebSocket reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
+
+ setTimeout(() => {
+ this.connect();
+ }, this.reconnectDelay);
+ }
}
diff --git a/src/app/services/challenge.service.ts b/src/app/services/challenge.service.ts
index 433f97d..5f7b809 100644
--- a/src/app/services/challenge.service.ts
+++ b/src/app/services/challenge.service.ts
@@ -5,46 +5,46 @@ import { Challenge, DeclineChallengeRequest, ListChallengesResponse, SendChallen
@Injectable({ providedIn: 'root' })
export class ChallengeService {
- private readonly http = inject(HttpClient);
- private readonly challengeBaseUrl = '/api/challenge';
+ private readonly http = inject(HttpClient);
+ private readonly challengeBaseUrl = '/api/challenge';
- sendChallenge(username: string, request: SendChallengeRequest): Observable {
- return this.http.post(
- `${this.challengeBaseUrl}/${username}`,
- request
- );
- }
+ sendChallenge(username: string, request: SendChallengeRequest): Observable {
+ return this.http.post(
+ `${this.challengeBaseUrl}/${username}`,
+ request
+ );
+ }
- listChallenges(): Observable {
- return this.http.get(
- `${this.challengeBaseUrl}`
- );
- }
+ listChallenges(): Observable {
+ return this.http.get(
+ `${this.challengeBaseUrl}`
+ );
+ }
- getChallenge(challengeId: string): Observable {
- return this.http.get(
- `${this.challengeBaseUrl}/${challengeId}`
- );
- }
+ getChallenge(challengeId: string): Observable {
+ return this.http.get(
+ `${this.challengeBaseUrl}/${challengeId}`
+ );
+ }
- acceptChallenge(challengeId: string): Observable {
- return this.http.post(
- `${this.challengeBaseUrl}/${challengeId}/accept`,
- {}
- );
- }
+ acceptChallenge(challengeId: string): Observable {
+ return this.http.post(
+ `${this.challengeBaseUrl}/${challengeId}/accept`,
+ {}
+ );
+ }
- declineChallenge(challengeId: string, request?: DeclineChallengeRequest): Observable {
- return this.http.post(
- `${this.challengeBaseUrl}/${challengeId}/decline`,
- request || {}
- );
- }
+ declineChallenge(challengeId: string, request?: DeclineChallengeRequest): Observable {
+ return this.http.post(
+ `${this.challengeBaseUrl}/${challengeId}/decline`,
+ request || {}
+ );
+ }
- cancelChallenge(challengeId: string): Observable {
- return this.http.post(
- `${this.challengeBaseUrl}/${challengeId}/cancel`,
- {}
- );
- }
+ cancelChallenge(challengeId: string): Observable {
+ return this.http.post(
+ `${this.challengeBaseUrl}/${challengeId}/cancel`,
+ {}
+ );
+ }
}