Users: get current user, onboard current user features

This commit is contained in:
2026-01-09 11:48:36 +01:00
parent 8fcc63e323
commit c6008cf84b
19 changed files with 358 additions and 2 deletions

View File

@@ -1,3 +1,5 @@
<app-navbar/>
<div class="section">
<div class="container">
<router-outlet />

View File

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

View File

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

View File

@@ -0,0 +1 @@
<p>home-view works!</p>

View 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 {
}

View 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>

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

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

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

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

View 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>

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

View File

@@ -0,0 +1 @@
<p>user-profile-view works!</p>

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

View File

@@ -1 +1,7 @@
@import 'bulma/css/versions/bulma-no-dark-mode.css';
html, body { min-height: 100%; }
mat-form-field {
width: 100%;
}