Users: get current user, onboard current user features
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
<app-navbar/>
|
||||
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<router-outlet />
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import {HomeView} from './home/views/home-view/home-view';
|
||||
import {OnboardingView} from './users/views/onboarding-view/onboarding-view';
|
||||
import {UserProfileView} from './users/views/user-profile-view/user-profile-view';
|
||||
|
||||
export const routes: Routes = [];
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: 'onboarding',
|
||||
component: OnboardingView
|
||||
},
|
||||
{
|
||||
path: 'user/:userId',
|
||||
component: UserProfileView
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '/home'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import {Navbar} from './shared/components/navbar/navbar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, Navbar],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.css'
|
||||
})
|
||||
|
||||
0
src/app/home/views/home-view/home-view.css
Normal file
0
src/app/home/views/home-view/home-view.css
Normal file
1
src/app/home/views/home-view/home-view.html
Normal file
1
src/app/home/views/home-view/home-view.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>home-view works!</p>
|
||||
11
src/app/home/views/home-view/home-view.ts
Normal file
11
src/app/home/views/home-view/home-view.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home-view',
|
||||
imports: [],
|
||||
templateUrl: './home-view.html',
|
||||
styleUrl: './home-view.css',
|
||||
})
|
||||
export class HomeView {
|
||||
|
||||
}
|
||||
0
src/app/shared/components/navbar/navbar.css
Normal file
0
src/app/shared/components/navbar/navbar.css
Normal file
32
src/app/shared/components/navbar/navbar.html
Normal file
32
src/app/shared/components/navbar/navbar.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<nav class="navbar is-fixed-top">
|
||||
<div class="navbar-brand"></div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start"></div>
|
||||
<div class="navbar-end">
|
||||
<!-- User menu dropdown -->
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a href="" class="navbar-link is-arrowless">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<div class="image is-24x24" [class.is-skeleton]="!sharedStore.isCurrentUserLoaded()">
|
||||
<img
|
||||
class="is-rounded"
|
||||
[src]="sharedStore.getCurrentUserAvatarUrl()"
|
||||
[alt]="sharedStore.getCurrentUserName()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p [class.is-skeleton]="!sharedStore.isCurrentUserLoaded()">
|
||||
{{sharedStore.getCurrentUserName()}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a (click)="logout()" class="navbar-item">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of user menu dropdown -->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
26
src/app/shared/components/navbar/navbar.ts
Normal file
26
src/app/shared/components/navbar/navbar.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
import {SharedStore} from '../../stores/shared.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
imports: [],
|
||||
templateUrl: './navbar.html',
|
||||
styleUrl: './navbar.css',
|
||||
})
|
||||
export class Navbar implements OnInit{
|
||||
|
||||
readonly sharedStore = inject(SharedStore)
|
||||
keycloak = inject(Keycloak)
|
||||
|
||||
ngOnInit() {
|
||||
this.sharedStore.loadCurrentUser()
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.keycloak.logout()
|
||||
}
|
||||
|
||||
}
|
||||
93
src/app/shared/stores/shared.store.ts
Normal file
93
src/app/shared/stores/shared.store.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
import {patchState, signalStore, withComputed, withMethods, withState} from '@ngrx/signals';
|
||||
|
||||
import {OnboardRequest, User} from '../../users/models/users.models';
|
||||
import {UserService} from '../../users/services/user';
|
||||
|
||||
interface SharedState {
|
||||
isCurrentUserLoaded: boolean
|
||||
isOnboarded: boolean
|
||||
currentUserId: string
|
||||
currentUser: User | null
|
||||
}
|
||||
|
||||
const initialState: SharedState = {
|
||||
isCurrentUserLoaded: false,
|
||||
isOnboarded: false,
|
||||
currentUser: null,
|
||||
currentUserId: ''
|
||||
}
|
||||
|
||||
export const SharedStore = signalStore(
|
||||
{providedIn: 'root'},
|
||||
withState(initialState),
|
||||
withMethods((store) => {
|
||||
let userService = inject(UserService)
|
||||
let router: Router = inject(Router)
|
||||
return {
|
||||
loadCurrentUser() {
|
||||
console.log('Loading current user...')
|
||||
userService.getCurrentUser().subscribe({
|
||||
next: result => {
|
||||
console.log(result)
|
||||
patchState(store, {
|
||||
isCurrentUserLoaded: true,
|
||||
currentUserId: result.id,
|
||||
currentUser: result,
|
||||
isOnboarded: result.onboarded
|
||||
})
|
||||
// If user is not onboarded yet, go to onboard page
|
||||
if (!result.onboarded) {
|
||||
console.log('Go to onboard page...')
|
||||
router.navigateByUrl('/onboarding')
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.log(err)
|
||||
patchState(store, {
|
||||
isCurrentUserLoaded: false,
|
||||
currentUser: null,
|
||||
currentUserId: '',
|
||||
isOnboarded: false
|
||||
})
|
||||
// Todo Go to error page
|
||||
}
|
||||
})
|
||||
},
|
||||
onboardCurrentUser(payload: OnboardRequest) {
|
||||
console.log('Handle onboarding...')
|
||||
userService.onboardUser(payload).subscribe({
|
||||
next: result => {
|
||||
patchState(store, {
|
||||
currentUser: result,
|
||||
isOnboarded: true,
|
||||
isCurrentUserLoaded: true,
|
||||
currentUserId: result.id
|
||||
})
|
||||
console.log('Navigate to user view page...')
|
||||
router.navigate(['/user', result.id])
|
||||
},
|
||||
error: err => {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}),
|
||||
withComputed((store) => {
|
||||
return {
|
||||
getCurrentUserName (): string {
|
||||
if (store.isCurrentUserLoaded()) {
|
||||
return `${store.currentUser()!.firstName} ${store.currentUser()!.lastName.substring(0,1)}`
|
||||
} else {
|
||||
return 'FirstName LastName'
|
||||
}
|
||||
},
|
||||
getCurrentUserAvatarUrl(): string {
|
||||
return 'avatar'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
16
src/app/users/models/users.models.ts
Normal file
16
src/app/users/models/users.models.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface User {
|
||||
id: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
headline: string
|
||||
avatarUrl: string
|
||||
onboarded: boolean
|
||||
active: boolean
|
||||
emailVerified: boolean
|
||||
}
|
||||
|
||||
export interface OnboardRequest {
|
||||
organizationName: string
|
||||
description: string
|
||||
student: boolean
|
||||
}
|
||||
25
src/app/users/services/user.ts
Normal file
25
src/app/users/services/user.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {OnboardRequest, User} from '../models/users.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
|
||||
httpClient: HttpClient = inject(HttpClient)
|
||||
|
||||
getCurrentUser(): Observable<User> {
|
||||
return this.httpClient.get<User>(`http://localhost:8000/users/current`)
|
||||
}
|
||||
|
||||
getUserById(userId: string): Observable<User> {
|
||||
return this.httpClient.get<User>(`http://localhost:8000/users/profile/${userId}`)
|
||||
}
|
||||
|
||||
onboardUser(payload: OnboardRequest): Observable<User>{
|
||||
return this.httpClient.post<User>(`http://localhost:8000/users/onboarding`, payload)
|
||||
}
|
||||
|
||||
}
|
||||
54
src/app/users/views/onboarding-view/onboarding-view.html
Normal file
54
src/app/users/views/onboarding-view/onboarding-view.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half is-full-mobile">
|
||||
<div class="box">
|
||||
<!-- Header -->
|
||||
<div class="block has-text-centered">
|
||||
<p class="title is-3">Tell more about you</p>
|
||||
</div>
|
||||
<!-- form -->
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
|
||||
<!-- Organization name -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>
|
||||
@if (isStudent()){
|
||||
<span>School name</span>
|
||||
} @else {
|
||||
<span>Employer</span>
|
||||
}
|
||||
</mat-label>
|
||||
<input matInput formControlName="organizationName">
|
||||
@if (form.get('organizationName')?.touched && form.get('organizationName')?.invalid ){
|
||||
<mat-error>This field cannot be blank</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Description -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>
|
||||
@if (isStudent()){
|
||||
<span>Field of study</span>
|
||||
} @else {
|
||||
<span>Position name</span>
|
||||
}
|
||||
</mat-label>
|
||||
<input matInput formControlName="description">
|
||||
@if (form.get('description')?.touched && form.get('description')?.invalid ){
|
||||
<mat-error>This field cannot be blank</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Student -->
|
||||
<div class="block">
|
||||
<mat-checkbox (change)="updateIsStudent($event.checked)" [checked]="isStudent()">I am student</mat-checkbox>
|
||||
</div>
|
||||
<!-- Buttons-->
|
||||
<div class="buttons">
|
||||
<button type="submit" matButton="outlined" [disabled]="form.invalid">Continue</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<!-- End of form-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
48
src/app/users/views/onboarding-view/onboarding-view.ts
Normal file
48
src/app/users/views/onboarding-view/onboarding-view.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {Component, inject, signal} from '@angular/core';
|
||||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {OnboardRequest} from '../../models/users.models';
|
||||
import {SharedStore} from '../../../shared/stores/shared.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-onboarding-view',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule
|
||||
],
|
||||
templateUrl: './onboarding-view.html',
|
||||
styleUrl: './onboarding-view.css',
|
||||
})
|
||||
export class OnboardingView {
|
||||
|
||||
sharedStore = inject(SharedStore)
|
||||
formBuilder: FormBuilder = inject(FormBuilder)
|
||||
|
||||
form: FormGroup = this.formBuilder.group({
|
||||
organizationName: ['', [Validators.required, Validators.maxLength(255)]],
|
||||
description: ['', [Validators.required, Validators.maxLength(255)]],
|
||||
})
|
||||
|
||||
isStudent = signal(false)
|
||||
|
||||
updateIsStudent(value: boolean){
|
||||
this.isStudent.set(value)
|
||||
}
|
||||
|
||||
submit() {
|
||||
const payload: OnboardRequest = {
|
||||
organizationName: this.form.get('organizationName')?.value,
|
||||
description: this.form.get('description')?.value,
|
||||
student: this.isStudent()
|
||||
}
|
||||
|
||||
this.sharedStore.onboardCurrentUser(payload)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>user-profile-view works!</p>
|
||||
19
src/app/users/views/user-profile-view/user-profile-view.ts
Normal file
19
src/app/users/views/user-profile-view/user-profile-view.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {Component, effect, input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-profile-view',
|
||||
imports: [],
|
||||
templateUrl: './user-profile-view.html',
|
||||
styleUrl: './user-profile-view.css',
|
||||
})
|
||||
export class UserProfileView {
|
||||
|
||||
userId = input.required<string>()
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
console.log('User id: ' + this.userId())
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
@import 'bulma/css/versions/bulma-no-dark-mode.css';
|
||||
|
||||
html, body { min-height: 100%; }
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user