Users: get current user, onboard current user features
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
<app-navbar/>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
import { Routes } from '@angular/router';
|
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 { Component, signal } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import {Navbar} from './shared/components/navbar/navbar';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet, Navbar],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.css'
|
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';
|
@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