Sistemato profilo

This commit is contained in:
Lorenzo Sanesi
2026-04-04 02:40:34 +02:00
parent 162e0de62a
commit aa5ca9bbe3
10 changed files with 454 additions and 28 deletions
@@ -25,7 +25,7 @@ class GroupsController {
async createGroup(req, res, next) { async createGroup(req, res, next) {
try { try {
const group = await this.groupsService.createGroup(req.body); const group = await this.groupsService.createGroup(req.body, req.userId);
return ResponseHandler.created(res, group, 'Group created successfully'); return ResponseHandler.created(res, group, 'Group created successfully');
} catch (error) { } catch (error) {
next(error); next(error);
@@ -14,6 +14,19 @@ class UsersController {
} }
} }
async searchUsers(req, res, next) {
try {
const { q } = req.query;
if (!q || !q.trim()) {
return ResponseHandler.badRequest(res, 'Query parameter "q" is required');
}
const users = await this.usersService.searchUsers(q.trim(), req.userId);
return ResponseHandler.success(res, users, 'Users found');
} catch (error) {
next(error);
}
}
async updateCurrentUser(req, res, next) { async updateCurrentUser(req, res, next) {
try { try {
const user = await this.usersService.updateUserProfile(req.userId, req.body); const user = await this.usersService.updateUserProfile(req.userId, req.body);
@@ -1,3 +1,4 @@
const { Op } = require('sequelize');
const { NotFoundError, DatabaseError } = require('../../shared/errors'); const { NotFoundError, DatabaseError } = require('../../shared/errors');
class UsersRepository { class UsersRepository {
@@ -6,6 +7,23 @@ class UsersRepository {
this.User = sequelize.models.User; this.User = sequelize.models.User;
} }
async search(query, excludeUserId) {
try {
return await this.User.findAll({
where: {
id: { [Op.ne]: excludeUserId },
[Op.or]: [
{ name: { [Op.iLike]: `%${query}%` } },
{ email: { [Op.iLike]: `%${query}%` } },
],
},
limit: 20,
});
} catch (error) {
throw new DatabaseError('Failed to search users', error);
}
}
async findAll(options = {}) { async findAll(options = {}) {
try { try {
return await this.User.findAll(options); return await this.User.findAll(options);
+2
View File
@@ -8,8 +8,10 @@ module.exports = ({ usersController }) => {
const controller = { const controller = {
getCurrentUser: (req, res, next) => usersController.getCurrentUser(req, res, next), getCurrentUser: (req, res, next) => usersController.getCurrentUser(req, res, next),
updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next), updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next),
searchUsers: (req, res, next) => usersController.searchUsers(req, res, next),
}; };
router.get('/search', controller.searchUsers);
router.get('/current', controller.getCurrentUser); router.get('/current', controller.getCurrentUser);
router.put('/current', validate(updateUserSchema), controller.updateCurrentUser); router.put('/current', validate(updateUserSchema), controller.updateCurrentUser);
+6 -5
View File
@@ -14,23 +14,24 @@ class GroupsService {
return await this.groupsRepository.findById(groupId, true); return await this.groupsRepository.findById(groupId, true);
} }
async createGroup(groupData) { async createGroup(groupData, creatorId) {
const { name, members } = groupData; const { name, members } = groupData;
if (!name || name.trim() === '') { if (!name || name.trim() === '') {
throw new ValidationError('Group name is required'); throw new ValidationError('Group name is required');
} }
if (!members || !Array.isArray(members) || members.length === 0) { const memberList = Array.isArray(members) ? [...members] : [];
throw new ValidationError('At least one member is required'); if (!memberList.includes(creatorId)) {
memberList.push(creatorId);
} }
for (const userId of members) { for (const userId of memberList) {
await this.usersRepository.findById(userId); await this.usersRepository.findById(userId);
} }
const group = await this.groupsRepository.create({ name }); const group = await this.groupsRepository.create({ name });
return await this.groupsRepository.addMembers(group.id, members); return await this.groupsRepository.addMembers(group.id, memberList);
} }
async updateGroup(groupId, updateData) { async updateGroup(groupId, updateData) {
+4
View File
@@ -13,6 +13,10 @@ class UsersService {
return await this.usersRepository.findById(id); return await this.usersRepository.findById(id);
} }
async searchUsers(query, excludeUserId) {
return await this.usersRepository.search(query, excludeUserId);
}
async updateUserProfile(id, updateData) { async updateUserProfile(id, updateData) {
const { name, email, avatarUrl } = updateData; const { name, email, avatarUrl } = updateData;
@@ -56,36 +56,81 @@
</section> </section>
} }
<!-- Friend Requests Section -->
@if (incomingRequests().length > 0) {
<section class="space-y-3">
<div class="flex items-center gap-2 px-1">
<h3 class="text-lg font-headline font-bold">Richieste di amicizia</h3>
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-primary text-on-primary text-[10px] font-black">{{ incomingRequests().length }}</span>
</div>
<div class="flex gap-3 overflow-x-auto pb-2 no-scrollbar">
@for (req of incomingRequests(); track req.id) {
<div class="flex-none w-36 bg-surface rounded-2xl p-3 flex flex-col items-center gap-2 border border-black/5 soft-shadow">
<div class="w-12 h-12 rounded-full bg-primary-container/20 flex items-center justify-center overflow-hidden">
@if (req.fromUser?.avatarUrl) {
<img [src]="req.fromUser!.avatarUrl" class="w-full h-full object-cover" [alt]="req.fromUser!.name">
} @else {
<mat-icon class="text-primary">person</mat-icon>
}
</div>
<p class="text-xs font-semibold text-center truncate w-full">{{ req.fromUser?.name }}</p>
@if (respondingToId() === req.id) {
<div class="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
} @else {
<div class="flex gap-2 w-full">
<button
(click)="acceptRequest(req)"
class="flex-1 flex items-center justify-center py-1.5 rounded-xl bg-primary text-on-primary">
<mat-icon style="font-size:16px;width:16px;height:16px;">check</mat-icon>
</button>
<button
(click)="rejectRequest(req)"
class="flex-1 flex items-center justify-center py-1.5 rounded-xl bg-error-container/30 text-error">
<mat-icon style="font-size:16px;width:16px;height:16px;">close</mat-icon>
</button>
</div>
}
</div>
}
</div>
</section>
}
<!-- Friends Section --> <!-- Friends Section -->
<section class="space-y-4"> <section class="space-y-4">
<div class="flex justify-between items-center px-1"> <div class="flex justify-between items-center px-1">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h3 class="text-lg font-headline font-bold">Amici</h3> <h3 class="text-lg font-headline font-bold">Amici</h3>
<button class="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors"> <button (click)="openAddFriendSheet()" class="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors">
<mat-icon class="text-xl">person_add</mat-icon> <mat-icon class="text-xl">person_add</mat-icon>
</button> </button>
</div> </div>
<button class="text-primary text-sm font-bold">Vedi tutti</button> <button (click)="openFriendsSheet()" class="text-primary text-sm font-bold">Vedi tutti</button>
</div> </div>
<div class="flex gap-3 overflow-x-auto pb-2 no-scrollbar"> <div class="flex gap-3 overflow-x-auto pb-2 no-scrollbar">
@for (friend of friends(); track friend.id) {
<div class="flex flex-col items-center gap-1 flex-none"> <div class="flex flex-col items-center gap-1 flex-none">
<div class="w-14 h-14 rounded-full border-2 border-primary p-0.5"> <div class="w-14 h-14 rounded-full border-2 border-primary p-0.5">
<img src="https://picsum.photos/seed/f1/100/100" class="w-full h-full rounded-full object-cover" alt="Friend Avatar"> @if (friend.avatarUrl) {
<img [src]="friend.avatarUrl" class="w-full h-full rounded-full object-cover" [alt]="friend.name">
} @else {
<div class="w-full h-full rounded-full bg-primary-container/20 flex items-center justify-center">
<mat-icon class="text-primary">person</mat-icon>
</div> </div>
<span class="text-[10px] font-medium text-on-surface-variant">Marco</span> }
</div> </div>
<div class="flex flex-col items-center gap-1 flex-none"> <span class="text-[10px] font-medium text-on-surface-variant">{{ friend.name }}</span>
<div class="w-14 h-14 rounded-full border-2 border-transparent p-0.5">
<img src="https://picsum.photos/seed/f2/100/100" class="w-full h-full rounded-full object-cover" alt="Friend Avatar">
</div> </div>
<span class="text-[10px] font-medium text-on-surface-variant">Luca</span> }
</div> @if (friends().length === 0) {
<div class="flex flex-col items-center gap-1 flex-none"> <p class="text-sm text-on-surface-variant italic">Nessun amico ancora</p>
}
<button (click)="openFriendsSheet()" class="flex flex-col items-center gap-1 flex-none">
<div class="w-14 h-14 rounded-full bg-surface-container flex items-center justify-center border-2 border-transparent border-dashed"> <div class="w-14 h-14 rounded-full bg-surface-container flex items-center justify-center border-2 border-transparent border-dashed">
<mat-icon class="text-on-surface-variant">search</mat-icon> <mat-icon class="text-on-surface-variant">search</mat-icon>
</div> </div>
<span class="text-[10px] font-medium text-on-surface-variant">Cerca</span> <span class="text-[10px] font-medium text-on-surface-variant">Cerca</span>
</div> </button>
</div> </div>
</section> </section>
@@ -147,3 +192,164 @@
</section> </section>
</main> </main>
</div> </div>
<!-- Friends List Bottom Sheet -->
@if (showFriendsSheet()) {
<div class="fixed inset-0 z-50 flex flex-col justify-end">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" (click)="closeFriendsSheet()"></div>
<div
class="relative bg-background shadow-2xl flex flex-col transition-all duration-300 ease-out"
[class]="friendsSheetExpanded() ? 'fixed inset-0 rounded-none h-full' : 'rounded-t-3xl h-[65vh]'">
<!-- Handle -->
<div
class="flex justify-center pt-3 pb-1 cursor-grab touch-none select-none"
(touchstart)="onHandleTouchStart($event)"
(touchend)="onFriendsHandleTouchEnd($event)">
<div class="w-10 h-1.5 rounded-full bg-on-surface/20"></div>
</div>
<!-- Header -->
<div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-headline font-bold">Amici ({{ friends().length }})</h3>
<button (click)="closeFriendsSheet()" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors">
<mat-icon class="text-on-surface-variant">close</mat-icon>
</button>
</div>
<!-- Search Input -->
<div class="px-6 pb-4">
<div class="flex items-center gap-3 bg-surface-container rounded-2xl px-4 py-3">
<mat-icon class="text-on-surface-variant flex-none">search</mat-icon>
<input
type="text"
placeholder="Cerca tra i tuoi amici..."
class="flex-1 bg-transparent text-sm outline-none text-on-surface placeholder:text-on-surface-variant"
[ngModel]="friendsSearchQuery()"
(ngModelChange)="friendsSearchQuery.set($event)"
autofocus
>
@if (friendsSearchQuery().trim()) {
<button (click)="friendsSearchQuery.set('')" class="flex-none">
<mat-icon class="text-on-surface-variant text-lg">close</mat-icon>
</button>
}
</div>
</div>
<!-- Friends List -->
<div class="overflow-y-auto flex-1 px-6 pb-8 space-y-1">
@if (filteredFriends().length > 0) {
@for (friend of filteredFriends(); track friend.id) {
<div class="w-full flex items-center gap-3 p-3 rounded-2xl">
<div class="w-11 h-11 rounded-full bg-primary-container/20 flex items-center justify-center flex-none overflow-hidden">
@if (friend.avatarUrl) {
<img [src]="friend.avatarUrl" class="w-full h-full object-cover" [alt]="friend.name">
} @else {
<mat-icon class="text-primary">person</mat-icon>
}
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-sm truncate">{{ friend.name }}</p>
<p class="text-xs text-on-surface-variant truncate">{{ friend.email }}</p>
</div>
<mat-icon class="text-secondary flex-none text-lg">people</mat-icon>
</div>
}
} @else if (friendsSearchQuery().trim()) {
<p class="text-sm text-on-surface-variant text-center py-8">Nessun amico trovato</p>
} @else {
<p class="text-sm text-on-surface-variant text-center py-8">Non hai ancora nessun amico</p>
}
</div>
</div>
</div>
}
<!-- Add Friend Bottom Sheet -->
@if (showAddFriendSheet()) {
<div class="fixed inset-0 z-50 flex flex-col justify-end">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" (click)="closeAddFriendSheet()"></div>
<!-- Sheet -->
<div
class="relative bg-background shadow-2xl flex flex-col transition-all duration-300 ease-out"
[class]="sheetExpanded() ? 'fixed inset-0 rounded-none h-full' : 'rounded-t-3xl h-[65vh]'">
<!-- Handle -->
<div
class="flex justify-center pt-3 pb-1 cursor-grab touch-none select-none"
(touchstart)="onHandleTouchStart($event)"
(touchend)="onHandleTouchEnd($event)">
<div class="w-10 h-1.5 rounded-full bg-on-surface/20"></div>
</div>
<!-- Header -->
<div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-headline font-bold">Aggiungi amico</h3>
<button (click)="closeAddFriendSheet()" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors">
<mat-icon class="text-on-surface-variant">close</mat-icon>
</button>
</div>
<!-- Search Input -->
<div class="px-6 pb-4">
<div class="flex items-center gap-3 bg-surface-container rounded-2xl px-4 py-3">
<mat-icon class="text-on-surface-variant flex-none">search</mat-icon>
<input
type="text"
placeholder="Cerca per nome o email..."
class="flex-1 bg-transparent text-sm outline-none text-on-surface placeholder:text-on-surface-variant"
[ngModel]="searchQuery()"
(ngModelChange)="onSearchChange($event)"
autofocus
>
@if (isSearching()) {
<div class="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin flex-none"></div>
}
</div>
</div>
<!-- Results -->
<div class="overflow-y-auto flex-1 px-6 pb-8 space-y-1">
@if (searchResults().length > 0) {
@for (user of searchResults(); track user.id) {
<button
class="w-full flex items-center gap-3 p-3 rounded-2xl transition-colors text-left"
[class]="isFriend(user.id) || sentToIds().has(user.id) ? 'opacity-60 cursor-default' : 'hover:bg-surface-container active:bg-surface-container-high cursor-pointer'"
[disabled]="isFriend(user.id) || sentToIds().has(user.id) || sendingToId() === user.id"
(click)="sendFriendRequest(user)">
<div class="w-11 h-11 rounded-full bg-primary-container/20 flex items-center justify-center flex-none overflow-hidden">
@if (user.avatarUrl) {
<img [src]="user.avatarUrl" class="w-full h-full object-cover" [alt]="user.name">
} @else {
<mat-icon class="text-primary">person</mat-icon>
}
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-sm truncate">{{ user.name }}</p>
<p class="text-xs text-on-surface-variant truncate">{{ user.email }}</p>
</div>
<div class="flex-none">
@if (sendingToId() === user.id) {
<div class="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
} @else if (sentToIds().has(user.id)) {
<mat-icon class="text-on-surface-variant text-lg">check_circle</mat-icon>
} @else if (isFriend(user.id)) {
<mat-icon class="text-secondary text-lg">people</mat-icon>
} @else {
<mat-icon class="text-primary text-lg">person_add</mat-icon>
}
</div>
</button>
}
} @else if (searchQuery().trim() && !isSearching()) {
<p class="text-sm text-on-surface-variant text-center py-8">Nessun utente trovato</p>
} @else if (!searchQuery().trim()) {
<p class="text-sm text-on-surface-variant text-center py-8">Cerca un utente per nome o email</p>
}
</div>
</div>
</div>
}
@@ -1,26 +1,59 @@
import {ChangeDetectionStrategy, Component, inject, signal, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, Component, inject, signal, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {MatIconModule} from '@angular/material/icon'; import {MatIconModule} from '@angular/material/icon';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {debounceTime, distinctUntilChanged, Subject, switchMap} from 'rxjs';
import {UserService} from '../../services/user'; import {UserService} from '../../services/user';
import {FriendService} from '../../services/friend.service';
import {LanguageService} from '../../services/language'; import {LanguageService} from '../../services/language';
import {User} from '../../models/data.models'; import {FriendRequest, User} from '../../models/data.models';
@Component({ @Component({
selector: 'app-profile', selector: 'app-profile',
standalone: true, standalone: true,
imports: [CommonModule, MatIconModule, RouterModule], imports: [CommonModule, FormsModule, MatIconModule, RouterModule],
templateUrl: './profile.html', templateUrl: './profile.html',
styleUrl: './profile.css', styleUrl: './profile.css',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class Profile implements OnInit { export class Profile implements OnInit {
private userService = inject(UserService); private userService = inject(UserService);
private friendService = inject(FriendService);
private langService = inject(LanguageService); private langService = inject(LanguageService);
userProfile = signal<User | null>(null); userProfile = signal<User | null>(null);
friends = signal<User[]>([]);
pendingRequests = signal<FriendRequest[]>([]);
respondingToId = signal<string | null>(null);
isLoading = signal(true); isLoading = signal(true);
// Add friend sheet
showAddFriendSheet = signal(false);
sheetExpanded = signal(false);
searchQuery = signal('');
searchResults = signal<User[]>([]);
isSearching = signal(false);
sendingToId = signal<string | null>(null);
sentToIds = signal<Set<string>>(new Set());
// Friends list sheet
showFriendsSheet = signal(false);
friendsSheetExpanded = signal(false);
friendsSearchQuery = signal('');
filteredFriends(): User[] {
const q = this.friendsSearchQuery().toLowerCase().trim();
if (!q) return this.friends();
return this.friends().filter(f =>
f.name.toLowerCase().includes(q) || f.email?.toLowerCase().includes(q)
);
}
private touchStartY = 0;
private searchInput$ = new Subject<string>();
ngOnInit() { ngOnInit() {
this.userService.getCurrentUser().subscribe({ this.userService.getCurrentUser().subscribe({
next: (user) => { next: (user) => {
@@ -32,6 +65,151 @@ export class Profile implements OnInit {
this.isLoading.set(false); this.isLoading.set(false);
} }
}); });
this.friendService.getFriends().subscribe({
next: (friends) => this.friends.set(friends),
error: (err) => console.error('Error loading friends:', err),
});
this.friendService.getPendingRequests().subscribe({
next: (requests) => this.pendingRequests.set(requests),
error: (err) => console.error('Error loading pending requests:', err),
});
this.searchInput$.pipe(
debounceTime(350),
distinctUntilChanged(),
switchMap((query) => {
if (!query.trim()) {
this.searchResults.set([]);
this.isSearching.set(false);
return [];
}
this.isSearching.set(true);
return this.userService.searchUsers(query);
})
).subscribe({
next: (results) => {
this.searchResults.set(results);
this.isSearching.set(false);
},
error: () => this.isSearching.set(false),
});
}
openAddFriendSheet() {
this.searchQuery.set('');
this.searchResults.set([]);
this.sentToIds.set(new Set());
this.sheetExpanded.set(false);
this.showAddFriendSheet.set(true);
}
closeAddFriendSheet() {
this.showAddFriendSheet.set(false);
this.sheetExpanded.set(false);
}
openFriendsSheet() {
this.friendsSearchQuery.set('');
this.friendsSheetExpanded.set(false);
this.showFriendsSheet.set(true);
}
closeFriendsSheet() {
this.showFriendsSheet.set(false);
this.friendsSheetExpanded.set(false);
}
onHandleTouchStart(event: TouchEvent) {
this.touchStartY = event.touches[0].clientY;
}
onHandleTouchEnd(event: TouchEvent) {
const deltaY = this.touchStartY - event.changedTouches[0].clientY;
if (deltaY > 40) {
this.sheetExpanded.set(true);
} else if (deltaY < -40) {
if (this.sheetExpanded()) {
this.sheetExpanded.set(false);
} else {
this.closeAddFriendSheet();
}
}
}
onFriendsHandleTouchEnd(event: TouchEvent) {
const deltaY = this.touchStartY - event.changedTouches[0].clientY;
if (deltaY > 40) {
this.friendsSheetExpanded.set(true);
} else if (deltaY < -40) {
if (this.friendsSheetExpanded()) {
this.friendsSheetExpanded.set(false);
} else {
this.closeFriendsSheet();
}
}
}
onSearchChange(query: string) {
this.searchQuery.set(query);
this.searchInput$.next(query);
}
sendFriendRequest(user: User) {
this.sendingToId.set(user.id);
this.friendService.sendRequest(user.id).subscribe({
next: () => {
this.sendingToId.set(null);
const updated = new Set(this.sentToIds());
updated.add(user.id);
this.sentToIds.set(updated);
},
error: (err) => {
console.error('Error sending friend request:', err);
this.sendingToId.set(null);
},
});
}
incomingRequests(): FriendRequest[] {
const currentId = this.userProfile()?.id;
return this.pendingRequests().filter(r => String(r.toUserId) === String(currentId));
}
acceptRequest(request: FriendRequest) {
this.respondingToId.set(request.id);
this.friendService.acceptRequest(Number(request.id)).subscribe({
next: () => {
this.pendingRequests.update(reqs => reqs.filter(r => r.id !== request.id));
if (request.fromUser) {
this.friends.update(f => [...f, request.fromUser!]);
}
this.respondingToId.set(null);
},
error: (err) => {
console.error('Error accepting request:', err);
this.respondingToId.set(null);
},
});
}
rejectRequest(request: FriendRequest) {
this.respondingToId.set(request.id);
this.friendService.rejectRequest(Number(request.id)).subscribe({
next: () => {
this.pendingRequests.update(reqs => reqs.filter(r => r.id !== request.id));
this.respondingToId.set(null);
},
error: (err) => {
console.error('Error rejecting request:', err);
this.respondingToId.set(null);
},
});
}
isFriend(userId: string): boolean {
return this.friends().some(f => f.id === userId);
} }
t(key: string): string { t(key: string): string {
@@ -110,6 +110,8 @@ export interface FriendRequest {
fromUserId: number; fromUserId: number;
toUserId: number; toUserId: number;
createdAt: string; createdAt: string;
fromUser?: User;
toUser?: User;
} }
export interface ActivityItem { export interface ActivityItem {
@@ -1,8 +1,10 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { FriendRequest } from '../models/data.models'; import { FriendRequest, User } from '../models/data.models';
import { SuccessResponse } from '../models/api-responses.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -11,12 +13,12 @@ export class FriendService {
private http = inject(HttpClient); private http = inject(HttpClient);
private readonly base = `${environment.apiUrl}/api/v1/friend-requests`; private readonly base = `${environment.apiUrl}/api/v1/friend-requests`;
getFriends(): Observable<FriendRequest[]> { getFriends(): Observable<User[]> {
return this.http.get<FriendRequest[]>(this.base); return this.http.get<SuccessResponse<User[]>>(this.base).pipe(map(res => res.data));
} }
getPendingRequests(): Observable<FriendRequest[]> { getPendingRequests(): Observable<FriendRequest[]> {
return this.http.get<FriendRequest[]>(`${this.base}/pending`); return this.http.get<SuccessResponse<FriendRequest[]>>(`${this.base}/pending`).pipe(map(res => res.data));
} }
sendRequest(identifier: string | number): Observable<FriendRequest> { sendRequest(identifier: string | number): Observable<FriendRequest> {