Added profile-header component. Implemented follower feature
This commit is contained in:
@@ -34,8 +34,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
"maximumWarning": "2MB",
|
||||
"maximumError": "3MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
@@ -79,4 +79,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
14
src/app/connections/models/connections.models.ts
Normal file
14
src/app/connections/models/connections.models.ts
Normal 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
|
||||
}
|
||||
25
src/app/connections/services/follower.ts
Normal file
25
src/app/connections/services/follower.ts
Normal 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}`)
|
||||
}
|
||||
|
||||
}
|
||||
89
src/app/connections/stores/follower.store.ts
Normal file
89
src/app/connections/stores/follower.store.ts
Normal 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})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
29
src/app/users/components/profile-header/profile-header.css
Normal file
29
src/app/users/components/profile-header/profile-header.css
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
56
src/app/users/components/profile-header/profile-header.html
Normal file
56
src/app/users/components/profile-header/profile-header.html
Normal 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>
|
||||
25
src/app/users/components/profile-header/profile-header.ts
Normal file
25
src/app/users/components/profile-header/profile-header.ts
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
74
src/app/users/stores/user.store.ts
Normal file
74
src/app/users/stores/user.store.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user