Sistemato profilo
This commit is contained in:
@@ -25,7 +25,7 @@ class GroupsController {
|
||||
|
||||
async createGroup(req, res, next) {
|
||||
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');
|
||||
} catch (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) {
|
||||
try {
|
||||
const user = await this.usersService.updateUserProfile(req.userId, req.body);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { Op } = require('sequelize');
|
||||
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||
|
||||
class UsersRepository {
|
||||
@@ -6,6 +7,23 @@ class UsersRepository {
|
||||
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 = {}) {
|
||||
try {
|
||||
return await this.User.findAll(options);
|
||||
|
||||
@@ -8,8 +8,10 @@ module.exports = ({ usersController }) => {
|
||||
const controller = {
|
||||
getCurrentUser: (req, res, next) => usersController.getCurrentUser(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.put('/current', validate(updateUserSchema), controller.updateCurrentUser);
|
||||
|
||||
|
||||
@@ -14,23 +14,24 @@ class GroupsService {
|
||||
return await this.groupsRepository.findById(groupId, true);
|
||||
}
|
||||
|
||||
async createGroup(groupData) {
|
||||
async createGroup(groupData, creatorId) {
|
||||
const { name, members } = groupData;
|
||||
|
||||
if (!name || name.trim() === '') {
|
||||
throw new ValidationError('Group name is required');
|
||||
}
|
||||
|
||||
if (!members || !Array.isArray(members) || members.length === 0) {
|
||||
throw new ValidationError('At least one member is required');
|
||||
const memberList = Array.isArray(members) ? [...members] : [];
|
||||
if (!memberList.includes(creatorId)) {
|
||||
memberList.push(creatorId);
|
||||
}
|
||||
|
||||
for (const userId of members) {
|
||||
for (const userId of memberList) {
|
||||
await this.usersRepository.findById(userId);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -13,6 +13,10 @@ class UsersService {
|
||||
return await this.usersRepository.findById(id);
|
||||
}
|
||||
|
||||
async searchUsers(query, excludeUserId) {
|
||||
return await this.usersRepository.search(query, excludeUserId);
|
||||
}
|
||||
|
||||
async updateUserProfile(id, updateData) {
|
||||
const { name, email, avatarUrl } = updateData;
|
||||
|
||||
|
||||
@@ -56,36 +56,81 @@
|
||||
</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 -->
|
||||
<section class="space-y-4">
|
||||
<div class="flex justify-between items-center px-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<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>
|
||||
</button>
|
||||
</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 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="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>
|
||||
<span class="text-[10px] font-medium text-on-surface-variant">Marco</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1 flex-none">
|
||||
<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">
|
||||
<span class="text-[10px] font-medium text-on-surface-variant">{{ friend.name }}</span>
|
||||
</div>
|
||||
<span class="text-[10px] font-medium text-on-surface-variant">Luca</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1 flex-none">
|
||||
}
|
||||
@if (friends().length === 0) {
|
||||
<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">
|
||||
<mat-icon class="text-on-surface-variant">search</mat-icon>
|
||||
</div>
|
||||
<span class="text-[10px] font-medium text-on-surface-variant">Cerca</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -147,3 +192,164 @@
|
||||
</section>
|
||||
</main>
|
||||
</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 {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {debounceTime, distinctUntilChanged, Subject, switchMap} from 'rxjs';
|
||||
import {UserService} from '../../services/user';
|
||||
import {FriendService} from '../../services/friend.service';
|
||||
import {LanguageService} from '../../services/language';
|
||||
import {User} from '../../models/data.models';
|
||||
import {FriendRequest, User} from '../../models/data.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatIconModule, RouterModule],
|
||||
imports: [CommonModule, FormsModule, MatIconModule, RouterModule],
|
||||
templateUrl: './profile.html',
|
||||
styleUrl: './profile.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Profile implements OnInit {
|
||||
private userService = inject(UserService);
|
||||
private friendService = inject(FriendService);
|
||||
private langService = inject(LanguageService);
|
||||
|
||||
userProfile = signal<User | null>(null);
|
||||
friends = signal<User[]>([]);
|
||||
pendingRequests = signal<FriendRequest[]>([]);
|
||||
respondingToId = signal<string | null>(null);
|
||||
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() {
|
||||
this.userService.getCurrentUser().subscribe({
|
||||
next: (user) => {
|
||||
@@ -32,6 +65,151 @@ export class Profile implements OnInit {
|
||||
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 {
|
||||
|
||||
@@ -110,6 +110,8 @@ export interface FriendRequest {
|
||||
fromUserId: number;
|
||||
toUserId: number;
|
||||
createdAt: string;
|
||||
fromUser?: User;
|
||||
toUser?: User;
|
||||
}
|
||||
|
||||
export interface ActivityItem {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
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({
|
||||
providedIn: 'root',
|
||||
@@ -11,12 +13,12 @@ export class FriendService {
|
||||
private http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiUrl}/api/v1/friend-requests`;
|
||||
|
||||
getFriends(): Observable<FriendRequest[]> {
|
||||
return this.http.get<FriendRequest[]>(this.base);
|
||||
getFriends(): Observable<User[]> {
|
||||
return this.http.get<SuccessResponse<User[]>>(this.base).pipe(map(res => res.data));
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
Reference in New Issue
Block a user