Sistemato profilo
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user