Modifiche

This commit is contained in:
Lorenzo Sanesi
2026-04-01 23:49:19 +02:00
parent 8752acd2c4
commit e2fe8b264f
52 changed files with 1351 additions and 400 deletions
+9 -2
View File
@@ -1,10 +1,11 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeComponent } from './features/home/home.component';
import { DiscoverComponent } from './features/discover/discover.component';
import { GroupsComponent } from './features/groups/groups.component';
import { ProfileComponent } from './features/profile/profile.component';
import { CreateBetComponent } from './features/bets/create-bet.component';
import { DataService } from './core/services/data.service';
type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
@@ -23,9 +24,15 @@ type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
private dataService = inject(DataService);
currentView = signal<View>('home');
private previousView = signal<View>('home');
constructor() {
this.dataService.loadCurrentUser();
}
setView(view: View) {
if (view === 'create-bet') {
this.previousView.set(this.currentView());
@@ -38,4 +45,4 @@ export class AppComponent {
onBetActionClosed() {
this.currentView.set(this.previousView());
}
}
}
@@ -0,0 +1,18 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { from, switchMap } from 'rxjs';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return from(authService.getValidToken()).pipe(
switchMap(token => {
if (!token) return next(req);
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next(authReq);
})
);
};
@@ -1,8 +1,3 @@
import { User } from './user.model';
import { Group } from './group.model';
import { Bet } from './bet.model';
import { FriendRequest } from './friend-request.model';
export interface SuccessResponse<T = any> {
success: boolean;
message?: string;
@@ -21,16 +16,3 @@ export interface ErrorResponse {
errors?: ErrorDetail[];
timestamp: string;
}
export interface InitialData {
currentUser: User;
users: User[];
friends: User[];
friendRequests: FriendRequest[];
receivedFriendRequests?: FriendRequest[]; // Legacy or convenience
sentFriendRequests?: FriendRequest[]; // Legacy or convenience
groups: Group[];
userBets: Bet[];
bets?: Bet[]; // Compatibility with data.service
publicBets: Bet[];
}
@@ -21,6 +21,18 @@ export class BetApiService {
private http = inject(HttpClient);
private readonly apiUrl = 'http://localhost:3000/api/v1/bets';
getUserBets(): Observable<Bet[]> {
return this.http.get<SuccessResponse<Bet[]>>(`${this.apiUrl}/user`).pipe(
map(res => res.data.map(mapBet))
);
}
getPublicBets(): Observable<Bet[]> {
return this.http.get<SuccessResponse<Bet[]>>(`${this.apiUrl}/public`).pipe(
map(res => res.data.map(mapBet))
);
}
getBets(): Observable<Bet[]> {
return this.http.get<SuccessResponse<Bet[]>>(this.apiUrl).pipe(
map(res => res.data.map(mapBet))
@@ -13,6 +13,18 @@ export class FriendRequestApiService {
private http = inject(HttpClient);
private readonly apiUrl = 'http://localhost:3000/api/v1/friend-requests';
getFriends(): Observable<User[]> {
return this.http.get<SuccessResponse<User[]>>(this.apiUrl).pipe(
map(res => res.data)
);
}
getPendingRequests(): Observable<FriendRequest[]> {
return this.http.get<SuccessResponse<FriendRequest[]>>(`${this.apiUrl}/pending`).pipe(
map(res => res.data)
);
}
sendRequest(request: SendFriendRequestRequest): Observable<FriendRequest> {
return this.http.post<SuccessResponse<FriendRequest>>(this.apiUrl, request).pipe(
map(res => res.data)
@@ -2,24 +2,8 @@ import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User, UpdateUserRequest } from '../../models/user.model';
import { SuccessResponse, InitialData } from '../../models/api-responses.model';
import { SuccessResponse } from '../../models/api-responses.model';
import { map } from 'rxjs/operators';
import { toDate } from '../../utils/type-guards';
function mapInitialData(data: InitialData): InitialData {
const mapBet = (bet: any) => ({
...bet,
startDate: toDate(bet.startDate)!,
endDate: toDate(bet.endDate)
});
return {
...data,
userBets: (data.userBets || []).map(mapBet),
publicBets: (data.publicBets || []).map(mapBet),
bets: (data.bets || []).map(mapBet)
};
}
@Injectable({
providedIn: 'root'
@@ -28,9 +12,9 @@ export class UserApiService {
private http = inject(HttpClient);
private readonly apiUrl = 'http://localhost:3000/api/v1';
getInitialData(): Observable<InitialData> {
return this.http.get<SuccessResponse<InitialData>>(`${this.apiUrl}/initial-data`).pipe(
map(res => mapInitialData(res.data))
getCurrentUser(): Observable<User> {
return this.http.get<SuccessResponse<User>>(`${this.apiUrl}/users/current`).pipe(
map(res => res.data)
);
}
@@ -39,6 +23,4 @@ export class UserApiService {
map(res => res.data)
);
}
// Potential future: getProfile(id), etc.
}
@@ -0,0 +1,27 @@
import { Injectable, inject } from '@angular/core';
import { KEYCLOAK_INSTANCE } from '../tokens/keycloak.token';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private keycloak = inject(KEYCLOAK_INSTANCE);
async getValidToken(): Promise<string> {
try {
await this.keycloak.updateToken(30);
} catch {
this.keycloak.login();
return '';
}
return this.keycloak.token!;
}
get tokenParsed() {
return this.keycloak.tokenParsed;
}
logout() {
this.keycloak.logout({ redirectUri: window.location.origin });
}
}
@@ -18,7 +18,6 @@ export class DataService {
private userApiService = inject(UserApiService);
private friendRequestApiService = inject(FriendRequestApiService);
private readonly _users = signal<User[]>([]);
private readonly _bets = signal<Bet[]>([]);
private readonly _groups = signal<Group[]>([]);
private readonly _friends = signal<User[]>([]);
@@ -47,26 +46,64 @@ export class DataService {
readonly getPublicBets = computed(() => this._publicBets());
constructor() {
this._initializeData();
// --- Load methods: chiamati da ogni componente al momento della visualizzazione ---
async loadCurrentUser(): Promise<void> {
try {
const user = await firstValueFrom(this.userApiService.getCurrentUser());
this.currentUser.set(user);
} catch (error) {
console.error('Failed to load current user', error);
}
}
private _initializeData() {
this.userApiService.getInitialData().subscribe({
next: data => {
this.currentUser.set(data.currentUser);
this._users.set(data.users);
this._friends.set(data.friends);
// Combine received and sent requests if they come separated, or use the array if unified
this._friendRequests.set(data.friendRequests || []);
this._bets.set(data.userBets || data.bets || []);
this._publicBets.set(data.publicBets);
this._groups.set(data.groups);
},
error: err => console.error("Failed to load initial data", err)
});
async loadUserBets(): Promise<void> {
try {
const bets = await firstValueFrom(this.betApiService.getUserBets());
this._bets.set(bets);
} catch (error) {
console.error('Failed to load user bets', error);
}
}
async loadPublicBets(): Promise<void> {
try {
const bets = await firstValueFrom(this.betApiService.getPublicBets());
this._publicBets.set(bets);
} catch (error) {
console.error('Failed to load public bets', error);
}
}
async loadFriends(): Promise<void> {
try {
const friends = await firstValueFrom(this.friendRequestApiService.getFriends());
this._friends.set(friends);
} catch (error) {
console.error('Failed to load friends', error);
}
}
async loadFriendRequests(): Promise<void> {
try {
const requests = await firstValueFrom(this.friendRequestApiService.getPendingRequests());
this._friendRequests.set(requests);
} catch (error) {
console.error('Failed to load friend requests', error);
}
}
async loadGroups(): Promise<void> {
try {
const groups = await firstValueFrom(this.groupApiService.getGroups());
this._groups.set(groups);
} catch (error) {
console.error('Failed to load groups', error);
}
}
// --- Utility ---
getGroupById(id: string): Group | undefined {
return this._groups().find(g => g.id === id);
}
@@ -75,6 +112,8 @@ export class DataService {
return this._bets().filter(b => b.groupId === groupId);
}
// --- Mutations ---
async sendFriendRequest(identifier: string): Promise<{ success: boolean, message: string }> {
try {
const newRequest = await firstValueFrom(this.friendRequestApiService.sendRequest({ identifier }));
@@ -90,7 +129,7 @@ export class DataService {
await firstValueFrom(this.friendRequestApiService.cancelRequest(requestId));
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
} catch (error) {
console.error("Failed to cancel friend request", error);
console.error('Failed to cancel friend request', error);
}
}
@@ -100,7 +139,7 @@ export class DataService {
this._friends.update(f => [...f, newFriend]);
this._friendRequests.update(reqs => reqs.filter(r => r.id !== request.id));
} catch (error) {
console.error("Failed to accept friend request", error);
console.error('Failed to accept friend request', error);
}
}
@@ -109,7 +148,7 @@ export class DataService {
await firstValueFrom(this.friendRequestApiService.declineRequest(requestId));
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
} catch (error) {
console.error("Failed to decline friend request", error);
console.error('Failed to decline friend request', error);
}
}
@@ -119,7 +158,7 @@ export class DataService {
const newGroup = await firstValueFrom(this.groupApiService.createGroup({ name, members: memberIds }));
this._groups.update(groups => [...groups, newGroup]);
} catch (error) {
console.error("Failed to add group", error);
console.error('Failed to add group', error);
}
}
@@ -128,7 +167,7 @@ export class DataService {
const updatedGroup = await firstValueFrom(this.groupApiService.removeMember(groupId, userId));
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
} catch (error) {
console.error("Failed to remove user from group", error);
console.error('Failed to remove user from group', error);
}
}
@@ -138,7 +177,7 @@ export class DataService {
const updatedGroup = await firstValueFrom(this.groupApiService.addMembers(groupId, { userIds }));
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
} catch (error) {
console.error("Failed to add users to group", error);
console.error('Failed to add users to group', error);
}
}
@@ -146,9 +185,8 @@ export class DataService {
try {
const updatedUser = await firstValueFrom(this.userApiService.updateCurrentUser(data));
this.currentUser.set(updatedUser);
this._users.update(users => users.map(u => u.id === updatedUser.id ? updatedUser : u));
} catch (error) {
console.error("Failed to update user", error);
console.error('Failed to update user', error);
}
}
@@ -180,7 +218,7 @@ export class DataService {
}));
this._bets.update(bets => [newBet, ...bets]);
} catch (error) {
console.error("Failed to add bet", error);
console.error('Failed to add bet', error);
}
}
}
@@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
import Keycloak from 'keycloak-js';
export const KEYCLOAK_INSTANCE = new InjectionToken<Keycloak>('KeycloakInstance');
@@ -2,7 +2,6 @@ import { Component, ChangeDetectionStrategy, output, inject, signal, computed, O
import { CommonModule } from '@angular/common';
import { DataService } from '../../core/services/data.service';
import { BetParticipant, BetRole } from '../../core/models/bet.model';
import { Group } from '../../core/models/group.model';
import { User } from '../../core/models/user.model';
import { getUserId } from '../../core/utils/type-guards';
@@ -51,6 +50,9 @@ export class CreateBetComponent implements OnInit {
});
ngOnInit() {
this.dataService.loadFriends();
this.dataService.loadGroups();
const user = this.currentUser();
if (user) {
this.participants.set([{ user, role: BetRole.Participant }]);
@@ -59,13 +61,13 @@ export class CreateBetComponent implements OnInit {
addParticipant(friend: User) {
this.participants.update(p => [...p, { user: friend, role: BetRole.Participant }]);
this.groupId.set(undefined); // Reset group if participants are manually changed
this.groupId.set(undefined);
this.showFriendSelector.set(false);
}
removeParticipant(userId: string) {
this.participants.update(p => p.filter(participant => getUserId(participant.user) !== userId));
this.groupId.set(undefined); // Reset group if participants are manually changed
this.groupId.set(undefined);
}
updateParticipantRole(userId: string, newRole: BetRole) {
@@ -94,7 +96,6 @@ export class CreateBetComponent implements OnInit {
this.participants.set(groupParticipants);
}
} else {
// Reset to just the current user if "Nessun gruppo" is selected
const user = this.currentUser();
if (user) {
this.participants.set([{ user, role: BetRole.Participant }]);
@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataService } from '../../core/services/data.service';
import { BetCardComponent } from '../../shared/components/bet-card/bet-card.component';
@@ -10,7 +10,11 @@ import { BetCardComponent } from '../../shared/components/bet-card/bet-card.comp
templateUrl: './discover.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DiscoverComponent {
export class DiscoverComponent implements OnInit {
private dataService = inject(DataService);
publicBets = this.dataService.getPublicBets;
}
readonly publicBets = this.dataService.getPublicBets;
ngOnInit() {
this.dataService.loadPublicBets();
}
}
@@ -1,10 +1,8 @@
import { Component, ChangeDetectionStrategy, input, output, inject, signal, computed, effect } from '@angular/core';
import { Component, ChangeDetectionStrategy, input, output, inject, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataService } from '../../../core/services/data.service';
import { Bet, BetStatus } from '../../../core/models/bet.model';
import { Group } from '../../../core/models/group.model';
import { User } from '../../../core/models/user.model';
import { BetCardComponent } from '../../../shared/components/bet-card/bet-card.component';
import { DataService } from '@/src/core/services/data.service';
import { Bet, BetStatus } from '@/src/core/models/bet.model';
import { BetCardComponent } from '@/src/shared/components/bet-card/bet-card.component';
@Component({
selector: 'app-group-detail',
@@ -22,30 +20,21 @@ export class GroupDetailComponent {
private allGroups = this.dataService.userGroups;
group = computed(() => this.allGroups().find(g => g.id === this.groupId()) ?? null);
groupBets = signal<Bet[]>([]);
readonly groupBets = computed(() => this.dataService.getBetsByGroupId(this.groupId()));
isAddingMembers = signal(false);
selectedFriendIds = signal<{ [key: string]: boolean }>({});
constructor() {
effect(() => {
const id = this.groupId();
if (id) {
this.groupBets.set(this.dataService.getBetsByGroupId(id));
}
});
}
sortedMembers = computed(() => {
return this.group()?.members.slice().sort((a, b) => b.score - a.score) ?? [];
});
activeBets = computed(() => {
return this.groupBets().filter(b => b.status === BetStatus.Active || b.status === BetStatus.AwaitingValidation);
return this.groupBets().filter((b: Bet) => b.status === BetStatus.Active || b.status === BetStatus.AwaitingValidation);
});
pastBets = computed(() => {
return this.groupBets().filter(b => b.status === BetStatus.Completed || b.status === BetStatus.Expired);
return this.groupBets().filter((b: Bet) => b.status === BetStatus.Completed || b.status === BetStatus.Expired);
});
availableFriends = computed(() => {
@@ -1,7 +1,6 @@
import { Component, ChangeDetectionStrategy, inject, signal, computed } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataService } from '../../core/services/data.service';
import { User } from '../../core/models/user.model';
import { GroupDetailComponent } from './group-detail/group-detail.component';
@Component({
@@ -11,7 +10,7 @@ import { GroupDetailComponent } from './group-detail/group-detail.component';
templateUrl: './groups.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GroupsComponent {
export class GroupsComponent implements OnInit {
private dataService = inject(DataService);
groups = this.dataService.userGroups;
@@ -27,6 +26,12 @@ export class GroupsComponent {
return this.newGroupName().trim().length > 0 && Object.keys(this.selectedFriendIds()).length > 0;
});
ngOnInit() {
this.dataService.loadGroups();
this.dataService.loadFriends();
this.dataService.loadUserBets();
}
selectGroup(groupId: string) {
this.selectedGroupId.set(groupId);
}
@@ -1,7 +1,6 @@
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataService } from '../../core/services/data.service';
import { Bet } from '../../core/models/bet.model';
import { BetsByTimeComponent } from './bets-by-time/bets-by-time.component';
import { BetsByGroupComponent } from './bets-by-group/bets-by-group.component';
@@ -16,12 +15,10 @@ export class HomeComponent implements OnInit {
private dataService = inject(DataService);
viewMode = signal<'time' | 'group'>('time');
allBets = signal<Bet[]>([]);
readonly allBets = this.dataService.getActiveBets;
ngOnInit() {
// FIX: getActiveBets is a computed signal which returns a value when called once.
// The extra parentheses caused an error because it tried to execute an array.
this.allBets.set(this.dataService.getActiveBets());
this.dataService.loadUserBets();
}
setViewMode(mode: 'time' | 'group') {
@@ -3,7 +3,7 @@
<!-- Header with Logout -->
<header class="flex justify-between items-center mb-4">
<h1 class="text-3xl font-bold">Profilo</h1>
<button title="Logout" class="p-2 text-slate-400 hover:text-white rounded-full hover:bg-slate-700 transition-colors">
<button (click)="logout()" title="Logout" class="p-2 text-slate-400 hover:text-white rounded-full hover:bg-slate-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
@@ -1,6 +1,7 @@
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DataService } from '../../core/services/data.service';
import { AuthService } from '../../core/services/auth.service';
import { EditProfileComponent } from './edit-profile/edit-profile.component';
import { FriendRequest } from '../../core/models/friend-request.model';
@@ -11,8 +12,9 @@ import { FriendRequest } from '../../core/models/friend-request.model';
templateUrl: './profile.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProfileComponent {
export class ProfileComponent implements OnInit {
private dataService = inject(DataService);
private authService = inject(AuthService);
currentUser = this.dataService.currentUser;
friends = this.dataService.friends;
@@ -26,6 +28,12 @@ export class ProfileComponent {
addFriendError = signal<string | null>(null);
addFriendSuccess = signal<string | null>(null);
ngOnInit() {
this.dataService.loadCurrentUser();
this.dataService.loadFriends();
this.dataService.loadFriendRequests();
}
toggleEditMode() {
this.isEditing.update(val => !val);
}
@@ -74,4 +82,8 @@ export class ProfileComponent {
declineRequest(requestId: string) {
this.dataService.declineFriendRequest(requestId);
}
logout() {
this.authService.logout();
}
}