Added profile-header component. Implemented follower feature
This commit is contained in:
@@ -34,8 +34,8 @@
|
|||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "500kB",
|
"maximumWarning": "2MB",
|
||||||
"maximumError": "1MB"
|
"maximumError": "3MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const devUrlCondition = createInterceptorCondition<IncludeBearerTokenCondition>(
|
|||||||
urlPattern: /^(http:\/\/localhost:8000)(\/.*)?$/i,
|
urlPattern: /^(http:\/\/localhost:8000)(\/.*)?$/i,
|
||||||
bearerPrefix: 'Bearer'
|
bearerPrefix: 'Bearer'
|
||||||
});
|
});
|
||||||
|
const devConnections = createInterceptorCondition<IncludeBearerTokenCondition>({
|
||||||
|
urlPattern: /^(http:\/\/localhost:8001)(\/.*)?$/i,
|
||||||
|
bearerPrefix: 'Bearer'
|
||||||
|
});
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -40,7 +44,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||||
useValue: [devUrlCondition,]
|
useValue: [devUrlCondition,devConnections]
|
||||||
},
|
},
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideRouter(routes, withComponentInputBinding()),
|
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...')
|
console.log('Loading current user...')
|
||||||
userService.getCurrentUser().subscribe({
|
userService.getCurrentUser().subscribe({
|
||||||
next: result => {
|
next: result => {
|
||||||
console.log(result)
|
console.log("current user " + result)
|
||||||
patchState(store, {
|
patchState(store, {
|
||||||
isCurrentUserLoaded: true,
|
isCurrentUserLoaded: true,
|
||||||
currentUserId: result.id,
|
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> {
|
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>{
|
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({
|
@Component({
|
||||||
selector: 'app-user-profile-view',
|
selector: 'app-user-profile-view',
|
||||||
imports: [],
|
imports: [
|
||||||
|
ProfileHeader
|
||||||
|
],
|
||||||
templateUrl: './user-profile-view.html',
|
templateUrl: './user-profile-view.html',
|
||||||
styleUrl: './user-profile-view.css',
|
styleUrl: './user-profile-view.css',
|
||||||
})
|
})
|
||||||
@@ -10,9 +15,13 @@ export class UserProfileView {
|
|||||||
|
|
||||||
userId = input.required<string>()
|
userId = input.required<string>()
|
||||||
|
|
||||||
|
readonly userStore = inject(UserStore)
|
||||||
|
readonly followerStore = inject(FollowerStore)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
console.log('User id: ' + this.userId())
|
this.userStore.loadUserProfile(this.userId())
|
||||||
|
this.followerStore.findFollower(this.userId())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user