From c1d401d2a9c9dc02da2e0990d505ecbff29e6236 Mon Sep 17 00:00:00 2001 From: Iurii Mednikov Date: Sun, 11 Jan 2026 13:00:23 +0100 Subject: [PATCH] Added profile-header component. Implemented follower feature --- angular.json | 6 +- src/app/app.config.ts | 6 +- .../connections/models/connections.models.ts | 14 +++ src/app/connections/services/follower.ts | 25 ++++++ src/app/connections/stores/follower.store.ts | 89 +++++++++++++++++++ src/app/shared/stores/shared.store.ts | 2 +- .../profile-header/profile-header.css | 29 ++++++ .../profile-header/profile-header.html | 56 ++++++++++++ .../profile-header/profile-header.ts | 25 ++++++ src/app/users/services/user.ts | 4 +- src/app/users/stores/user.store.ts | 74 +++++++++++++++ .../user-profile-view/user-profile-view.html | 13 ++- .../user-profile-view/user-profile-view.ts | 15 +++- 13 files changed, 347 insertions(+), 11 deletions(-) create mode 100644 src/app/connections/models/connections.models.ts create mode 100644 src/app/connections/services/follower.ts create mode 100644 src/app/connections/stores/follower.store.ts create mode 100644 src/app/users/components/profile-header/profile-header.css create mode 100644 src/app/users/components/profile-header/profile-header.html create mode 100644 src/app/users/components/profile-header/profile-header.ts create mode 100644 src/app/users/stores/user.store.ts diff --git a/angular.json b/angular.json index 9a978be..321e68f 100644 --- a/angular.json +++ b/angular.json @@ -34,8 +34,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "2MB", + "maximumError": "3MB" }, { "type": "anyComponentStyle", @@ -79,4 +79,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 10c902b..83c4ef4 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -18,6 +18,10 @@ const devUrlCondition = createInterceptorCondition( urlPattern: /^(http:\/\/localhost:8000)(\/.*)?$/i, bearerPrefix: 'Bearer' }); +const devConnections = createInterceptorCondition({ + urlPattern: /^(http:\/\/localhost:8001)(\/.*)?$/i, + bearerPrefix: 'Bearer' +}); export const appConfig: ApplicationConfig = { providers: [ @@ -40,7 +44,7 @@ export const appConfig: ApplicationConfig = { }), { provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG, - useValue: [devUrlCondition,] + useValue: [devUrlCondition,devConnections] }, provideBrowserGlobalErrorListeners(), provideRouter(routes, withComponentInputBinding()), diff --git a/src/app/connections/models/connections.models.ts b/src/app/connections/models/connections.models.ts new file mode 100644 index 0000000..4bc59e9 --- /dev/null +++ b/src/app/connections/models/connections.models.ts @@ -0,0 +1,14 @@ +import {User} from '../../users/models/users.models'; + +export interface Follower { + id: string + ownerId: string + followedUser: User + mutual: boolean + muted: boolean +} + +export interface CreateFollowerRequest { + ownerId: string + followedUserId: string +} diff --git a/src/app/connections/services/follower.ts b/src/app/connections/services/follower.ts new file mode 100644 index 0000000..2b9a859 --- /dev/null +++ b/src/app/connections/services/follower.ts @@ -0,0 +1,25 @@ +import {inject, Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {CreateFollowerRequest, Follower} from '../models/connections.models'; + +@Injectable({ + providedIn: 'root', +}) +export class FollowerService { + + httpClient: HttpClient = inject(HttpClient) + + createFollower (payload: CreateFollowerRequest): Observable { + return this.httpClient.post(`http://localhost:8001/followers/create`, payload) + } + + deleteFollower (id: string): Observable { + return this.httpClient.delete(`http://localhost:8001/followers/delete/${id}`) + } + + findFollower (ownerId: string, followedId: string): Observable { + return this.httpClient.get(`http://localhost:8001/followers/find?followerId=${ownerId}&followedId=${followedId}`) + } + +} diff --git a/src/app/connections/stores/follower.store.ts b/src/app/connections/stores/follower.store.ts new file mode 100644 index 0000000..df3a595 --- /dev/null +++ b/src/app/connections/stores/follower.store.ts @@ -0,0 +1,89 @@ +import {inject} from '@angular/core'; +import {patchState, signalStore, withMethods, withState} from '@ngrx/signals'; +import {SharedStore} from '../../shared/stores/shared.store'; +import {FollowerService} from '../services/follower'; +import {CreateFollowerRequest, Follower} from '../models/connections.models'; + +interface FollowerState { + userId: string, + isLoaded: boolean + isSubscribed: boolean + follower: Follower | null +} + +const initialState: FollowerState = { + userId: '', + isLoaded: false, + isSubscribed: false, + follower: null +} + +export const FollowerStore = signalStore( + {providedIn: 'root'}, + withState(initialState), + withMethods((store) => { + const sharedStore = inject(SharedStore) + const followerService = inject(FollowerService) + return { + + followUser (){ + const currentUserId = sharedStore.currentUserId() + if (store.userId() != currentUserId) { + const payload: CreateFollowerRequest = { + ownerId: currentUserId, + followedUserId: store.userId() + } + + // Execute request + followerService.createFollower(payload).subscribe({ + next: result => { + console.log(result) + patchState(store, {isLoaded: true, isSubscribed: true, follower: result}) + }, + error: err =>{ + console.log(err) + } + }) + + } + }, + unfollowUser (){ + if (store.isSubscribed()) { + const id = store.follower()!.id + // Execute request + followerService.deleteFollower(id).subscribe({ + next: result => { + console.log(result) + patchState(store, {isLoaded: true, isSubscribed: false, follower: null}) + }, + error: err =>{ + console.log(err) + } + }) + } + + }, + findFollower(userId: string){ + patchState(store, {userId: userId}) + const isCurrentLoaded = sharedStore.isCurrentUserLoaded() + const currentUserId = sharedStore.currentUserId() + if (isCurrentLoaded && userId != currentUserId) { + console.log("Check for follower...") + // load from server + followerService.findFollower(currentUserId, userId).subscribe({ + next: result => { + console.log(result) + patchState(store, {isLoaded: true, isSubscribed: true, follower: result}) + }, + error: err => { + console.log(err) + patchState(store, {isLoaded: true, isSubscribed: false, follower: null}) + } + }) + + } + } + + } + }) +) diff --git a/src/app/shared/stores/shared.store.ts b/src/app/shared/stores/shared.store.ts index 6950da4..71864e4 100644 --- a/src/app/shared/stores/shared.store.ts +++ b/src/app/shared/stores/shared.store.ts @@ -31,7 +31,7 @@ export const SharedStore = signalStore( console.log('Loading current user...') userService.getCurrentUser().subscribe({ next: result => { - console.log(result) + console.log("current user " + result) patchState(store, { isCurrentUserLoaded: true, currentUserId: result.id, diff --git a/src/app/users/components/profile-header/profile-header.css b/src/app/users/components/profile-header/profile-header.css new file mode 100644 index 0000000..24726c0 --- /dev/null +++ b/src/app/users/components/profile-header/profile-header.css @@ -0,0 +1,29 @@ +.app-profile-header-avatar { + margin-top: -100px; + height: 160px; + width: 160px; + overflow: hidden; +} + +.app-profile-header-cover { + height: 180px; + overflow: hidden; +} + +.app-profile-header-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.app-profile-header-avatar img { + border: 1px solid white; +} + +@media (min-width: 1024px) { + + .app-profile-name { + max-width: 400px; + } + +} diff --git a/src/app/users/components/profile-header/profile-header.html b/src/app/users/components/profile-header/profile-header.html new file mode 100644 index 0000000..fadcb20 --- /dev/null +++ b/src/app/users/components/profile-header/profile-header.html @@ -0,0 +1,56 @@ +
+ +
+
+ + +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+

{{ userStore.getUserFullName() }}

+

{{ userStore.user()?.headline }}

+
+
+
+ + @if (userStore.isUserLoaded()){ +
Featured
+ } +
+
+ + +
+ @if (userStore.isCurrentUser()){ +
+ +
+ } @else { +
+ @if (followerStore.isLoaded()){ + @if (followerStore.isSubscribed()){ + + + } @else { + + + } + } +
+ } +
+
+ +
diff --git a/src/app/users/components/profile-header/profile-header.ts b/src/app/users/components/profile-header/profile-header.ts new file mode 100644 index 0000000..e6d3351 --- /dev/null +++ b/src/app/users/components/profile-header/profile-header.ts @@ -0,0 +1,25 @@ +import {Component, inject} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; +import {UserStore} from '../../stores/user.store'; +import {FollowerStore} from '../../../connections/stores/follower.store'; + +@Component({ + selector: 'app-profile-header', + imports: [MatButtonModule], + templateUrl: './profile-header.html', + styleUrl: './profile-header.css', +}) +export class ProfileHeader { + + readonly userStore = inject(UserStore) + readonly followerStore = inject(FollowerStore) + + onUnfollowClicked(){ + this.followerStore.unfollowUser() + } + + onFollowClicked() { + this.followerStore.followUser() + } + +} diff --git a/src/app/users/services/user.ts b/src/app/users/services/user.ts index 5cef5a0..e010485 100644 --- a/src/app/users/services/user.ts +++ b/src/app/users/services/user.ts @@ -15,11 +15,11 @@ export class UserService { } getUserById(userId: string): Observable { - return this.httpClient.get(`http://localhost:8000/users/profile/${userId}`) + return this.httpClient.get(`http://localhost:8000/profiles/user/${userId}`) } onboardUser(payload: OnboardRequest): Observable{ - return this.httpClient.post(`http://localhost:8000/users/onboarding`, payload) + return this.httpClient.post(`http://localhost:8000/profiles/onboard`, payload) } } diff --git a/src/app/users/stores/user.store.ts b/src/app/users/stores/user.store.ts new file mode 100644 index 0000000..91b0990 --- /dev/null +++ b/src/app/users/stores/user.store.ts @@ -0,0 +1,74 @@ +import {Router} from '@angular/router'; +import {inject} from '@angular/core'; + +import {patchState, signalStore, withComputed, withMethods, withState} from '@ngrx/signals'; + +import {User} from '../models/users.models'; +import {UserService} from '../services/user'; +import {SharedStore} from '../../shared/stores/shared.store'; + +interface UserState { + isUserLoaded: boolean + isCurrentUser: boolean + userId: string + user: User | null +} + +const initialState: UserState = { + isUserLoaded: false, + isCurrentUser: false, + userId: '', + user: null +} + +export const UserStore = signalStore( + {providedIn: 'root'}, + withState(initialState), + withMethods((store) => { + let router: Router = inject(Router) + let userService: UserService = inject(UserService) + let sharedStore = inject(SharedStore) + return { + loadUserProfile(userId: string){ + // Update store + patchState(store, { + userId: userId, + isUserLoaded: false, + isCurrentUser: false, + user: null + }) + // Get user from server + userService.getUserById(userId).subscribe({ + next: result => { + console.log("user: " + result) + let isCurrent = sharedStore.currentUserId() == userId + patchState(store, { + userId: userId, + isUserLoaded: true, + isCurrentUser: isCurrent, + user: result + }) + }, + error: (err) => { + console.log(err) + // Todo go to not found page + } + }) + } + } + }), + withComputed((store) => { + return { + getUserFullName(): string { + if (store.isUserLoaded()){ + return `${store.user()!.firstName} ${store.user()!.lastName}` + } else { + return 'FirstName LastName' + } + }, + getAvatarUrl(): string { + return 'avatar' + } + } + }) +) diff --git a/src/app/users/views/user-profile-view/user-profile-view.html b/src/app/users/views/user-profile-view/user-profile-view.html index 2eeb553..26268dc 100644 --- a/src/app/users/views/user-profile-view/user-profile-view.html +++ b/src/app/users/views/user-profile-view/user-profile-view.html @@ -1 +1,12 @@ -

user-profile-view works!

+
+ +
+ +
+ +
+ +
+ +
Recommendations
+
diff --git a/src/app/users/views/user-profile-view/user-profile-view.ts b/src/app/users/views/user-profile-view/user-profile-view.ts index 2e1b210..ff9ae27 100644 --- a/src/app/users/views/user-profile-view/user-profile-view.ts +++ b/src/app/users/views/user-profile-view/user-profile-view.ts @@ -1,8 +1,13 @@ -import {Component, effect, input} from '@angular/core'; +import {Component, effect, inject, input} from '@angular/core'; +import {ProfileHeader} from '../../components/profile-header/profile-header'; +import {UserStore} from '../../stores/user.store'; +import {FollowerStore} from '../../../connections/stores/follower.store'; @Component({ selector: 'app-user-profile-view', - imports: [], + imports: [ + ProfileHeader + ], templateUrl: './user-profile-view.html', styleUrl: './user-profile-view.css', }) @@ -10,9 +15,13 @@ export class UserProfileView { userId = input.required() + readonly userStore = inject(UserStore) + readonly followerStore = inject(FollowerStore) + constructor() { effect(() => { - console.log('User id: ' + this.userId()) + this.userStore.loadUserProfile(this.userId()) + this.followerStore.findFollower(this.userId()) }) }