Added profile-header component. Implemented follower feature

This commit is contained in:
2026-01-11 13:00:23 +01:00
parent c6008cf84b
commit c1d401d2a9
13 changed files with 347 additions and 11 deletions

View File

@@ -34,8 +34,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
"maximumWarning": "2MB",
"maximumError": "3MB"
},
{
"type": "anyComponentStyle",
@@ -79,4 +79,4 @@
}
}
}
}
}

View File

@@ -18,6 +18,10 @@ const devUrlCondition = createInterceptorCondition<IncludeBearerTokenCondition>(
urlPattern: /^(http:\/\/localhost:8000)(\/.*)?$/i,
bearerPrefix: 'Bearer'
});
const devConnections = createInterceptorCondition<IncludeBearerTokenCondition>({
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()),

View File

@@ -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
}

View File

@@ -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<Follower> {
return this.httpClient.post<Follower>(`http://localhost:8001/followers/create`, payload)
}
deleteFollower (id: string): Observable<void> {
return this.httpClient.delete<void>(`http://localhost:8001/followers/delete/${id}`)
}
findFollower (ownerId: string, followedId: string): Observable<Follower> {
return this.httpClient.get<Follower>(`http://localhost:8001/followers/find?followerId=${ownerId}&followedId=${followedId}`)
}
}

View File

@@ -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})
}
})
}
}
}
})
)

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -0,0 +1,56 @@
<div class="card">
<div class="card-image">
<div class="image app-profile-header-cover" [class.is-skeleton]="!userStore.isUserLoaded()">
<!-- Todo change cover image -->
<img src="https://images.pexels.com/photos/1103970/pexels-photo-1103970.jpeg" alt="">
</div>
</div>
<div class="card-content">
<!-- Avatar -->
<div class="image is-128x128 app-profile-header-avatar block" [class.is-skeleton]="!userStore.isUserLoaded()">
<img [src]="userStore.getAvatarUrl()" [alt]="userStore.getUserFullName()" class="is-rounded">
</div>
<!-- Main -->
<div class="block">
<div class="level">
<!-- Name -->
<div class="level-left">
<div class="level-item">
<div class="block app-profile-name">
<p class="title is-3" [class.is-skeleton]="!userStore.isUserLoaded()">{{ userStore.getUserFullName() }}</p>
<p class="subtitle is-5" [class.is-skeleton]="!userStore.isUserLoaded()">{{ userStore.user()?.headline }}</p>
</div>
</div>
</div>
<!-- Featured -->
@if (userStore.isUserLoaded()){
<div class="level-right">Featured</div>
}
</div>
</div>
<!-- Actions -->
<div class="block">
@if (userStore.isCurrentUser()){
<div class="buttons">
<button type="button" matButton="outlined">Edit profile</button>
</div>
} @else {
<div class="buttons">
@if (followerStore.isLoaded()){
@if (followerStore.isSubscribed()){
<!-- Subscribed -->
<button type="button" matButton="outlined" (click)="onUnfollowClicked()">Unfollow</button>
} @else {
<!-- Not subscribed -->
<button type="button" matButton="filled" (click)="onFollowClicked()">Follow</button>
}
}
</div>
}
</div>
</div>
</div>

View File

@@ -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()
}
}

View File

@@ -15,11 +15,11 @@ export class UserService {
}
getUserById(userId: string): Observable<User> {
return this.httpClient.get<User>(`http://localhost:8000/users/profile/${userId}`)
return this.httpClient.get<User>(`http://localhost:8000/profiles/user/${userId}`)
}
onboardUser(payload: OnboardRequest): Observable<User>{
return this.httpClient.post<User>(`http://localhost:8000/users/onboarding`, payload)
return this.httpClient.post<User>(`http://localhost:8000/profiles/onboard`, payload)
}
}

View File

@@ -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'
}
}
})
)

View File

@@ -1 +1,12 @@
<p>user-profile-view works!</p>
<div class="columns is-centered">
<!-- Main column -->
<div class="column is-6 is-full-mobile">
<!-- Profile header -->
<div class="block">
<app-profile-header/>
</div>
<!-- -->
</div>
<!-- Recommendations -->
<div class="column is-3 is-full-mobile">Recommendations</div>
</div>

View File

@@ -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<string>()
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())
})
}