Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile Pages #2

Merged
merged 24 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions src/@seed/api/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import type { HttpErrorResponse } from '@angular/common/http'
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import type { Observable } from 'rxjs'
import { distinctUntilChanged, ReplaySubject, switchMap, take, tap } from 'rxjs'
import type { CurrentUser, SetDefaultOrganizationResponse } from '@seed/api/user'
import { catchError, distinctUntilChanged, ReplaySubject, switchMap, take, tap, throwError } from 'rxjs'
import type {
CurrentUser,
GenerateApiKeyResponse,
PasswordUpdateRequest,
PasswordUpdateResponse,
SetDefaultOrganizationResponse,
UserUpdateRequest,
} from '@seed/api/user'

@Injectable({ providedIn: 'root' })
export class UserService {
Expand Down Expand Up @@ -42,4 +50,53 @@ export class UserService {
}),
)
}

/**
* Update user
*/
updateUser(userId: number, params: UserUpdateRequest): Observable<CurrentUser> {
return this._httpClient.put<CurrentUser>(`api/v3/users/${userId}/`, params).pipe(
tap((user) => {
this._currentUser.next(user)
}),
catchError((error: HttpErrorResponse) => {
console.error('Error occurred while updating user:', error.error)
return this._currentUser
}),
)
}

/**
* Update user
*/
updatePassword(params: PasswordUpdateRequest): Observable<PasswordUpdateResponse> {
return this.currentUser$.pipe(
take(1),
switchMap(({ id: userId }) => {
return this._httpClient.put<PasswordUpdateResponse>(`api/v3/users/${userId}/set_password/`, params)
}),
tap(() => {
this.getCurrentUser().subscribe()
}),
catchError((error: HttpErrorResponse) => {
return throwError(() => error)
}),
)
}

/**
* Generate API Key
*/
generateApiKey(): Observable<GenerateApiKeyResponse> {
return this.currentUser$.pipe(
take(1),
switchMap(({ id: userId }) => {
return this._httpClient.post<GenerateApiKeyResponse>(`api/v3/users/${userId}/generate_api_key/`, {})
}),
tap(() => {
// Refresh user info after changing the API key
this.getCurrentUser().subscribe()
}),
)
}
}
21 changes: 21 additions & 0 deletions src/@seed/api/user/user.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ export type CurrentUser = {
is_ali_leaf: boolean;
}

export type UserUpdateRequest = {
first_name: string;
last_name: string;
email: string;
}

export type PasswordUpdateRequest = {
current_password: string;
password_1: string;
password_2: string;
}

export type SetDefaultOrganizationResponse = {
status: string;
user: {
Expand All @@ -29,3 +41,12 @@ export type SetDefaultOrganizationResponse = {
};
};
}

export type GenerateApiKeyResponse = {
status: string;
api_key: string;
}

export type PasswordUpdateResponse = {
status: string;
}
6 changes: 6 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AboutComponent } from './modules/main/about/about.component'
import { ContactComponent } from './modules/main/contact/contact.component'
import { DocumentationComponent } from './modules/main/documentation/documentation.component'
import { HomeComponent } from './modules/main/home/home.component'
import { ProfileComponent } from './modules/profile/profile.component'

const inventoryTypeMatcher = (segments: UrlSegment[]) => {
if (segments.length === 1 && ['properties', 'taxlots'].includes(segments[0].path)) {
Expand Down Expand Up @@ -60,6 +61,11 @@ export const appRoutes: Route[] = [
title: 'Dashboard',
component: HomeComponent,
},
{
path: 'profile',
component: ProfileComponent,
loadChildren: () => import('app/modules/profile/profile.routes'),
},
{
matcher: inventoryTypeMatcher,
loadChildren: () => import('app/modules/inventory/inventory.routes'),
Expand Down
4 changes: 2 additions & 2 deletions src/app/core/auth/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export const authInterceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn):
})

return next(newReq).pipe(
catchError(() => {
return throwError(() => new Error(`Failed request ${req.method} ${req.url}`))
catchError((error: HttpErrorResponse) => {
return throwError(() => error)
}),
)
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout/common/user/user.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</span>
</div>
<mat-divider class="my-2"></mat-divider>
<button mat-menu-item>
<button mat-menu-item (click)="goToProfile()">
<mat-icon svgIcon="fa-solid:circle-user"></mat-icon>
<span>Profile</span>
</button>
Expand Down
6 changes: 6 additions & 0 deletions src/app/layout/common/user/user.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'
import { MatDividerModule } from '@angular/material/divider'
import { MatIconModule } from '@angular/material/icon'
import { MatMenuModule } from '@angular/material/menu'
import { Router } from '@angular/router'
import { Subject, takeUntil } from 'rxjs'
import type { CurrentUser } from '@seed/api/user'
import { UserService } from '@seed/api/user'
Expand All @@ -21,6 +22,7 @@ import { AuthService } from 'app/core/auth/auth.service'
export class UserComponent implements OnInit, OnDestroy {
private _authService = inject(AuthService)
private _changeDetectorRef = inject(ChangeDetectorRef)
private _router = inject(Router)
private _userService = inject(UserService)

showAvatar = input(true, { transform: booleanAttribute })
Expand Down Expand Up @@ -48,4 +50,8 @@ export class UserComponent implements OnInit, OnDestroy {
signOut(): void {
this._authService.signOut()
}

goToProfile() {
void this._router.navigate(['/profile'])
}
}
4 changes: 4 additions & 0 deletions src/app/modules/profile/admin/admin.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div id="admin" class="tab-pane fade" *transloco="let t">
<h1 class="mb-2 text-xl font-bold">Admin</h1>
<p>Admin content goes here.</p>
</div>
9 changes: 9 additions & 0 deletions src/app/modules/profile/admin/admin.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from '@angular/core'
import { SharedImports } from '@seed/directives'

@Component({
selector: 'seed-admin',
templateUrl: './admin.component.html',
imports: [SharedImports],
})
export class AdminComponent {}
40 changes: 40 additions & 0 deletions src/app/modules/profile/developer/developer.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<div class="flex-auto p-6 sm:p-10" *transloco="let t">
<div class="max-w-2xl">
<h1 class="mb-6 flex items-center text-center text-2xl"><mat-icon class="mr-2" svgIcon="fa-solid:code"></mat-icon>Developer</h1>

<div class="bg-card mt-8 flex flex-col overflow-hidden rounded-2xl p-8 pb-4 shadow">
<h2 class="pb-5 text-xl">{{ t('Manage Your API Key') }}</h2>

<div class="gt-xs:flex-row flex flex-col pt-4 text-center">
<div>
<span class="font-bold">API Key: </span>
<span class="bg-slate-200 px-5 py-3">{{ user.api_key }}</span>
</div>
<div class="pt-7">
<button mat-flat-button color="primary" (click)="generateKey()">
<span class="ml-2">{{ t('Get a New API Key') }}</span>
</button>
</div>
</div>

<!-- Alert -->
@if (showAlert) {
<div class="mb-4 mt-0">
<seed-alert class="-mb-4 mt-8" appearance="outline" [showIcon]="false" [type]="alert.type">
{{ alert.message }}
</seed-alert>
</div>
}

<!-- Divider -->
<div class="mb-5 mt-10 border-t"></div>
<h3 class="text-l mb-5">{{ t('Example Usage') }}</h3>
<pre class="border-2 bg-slate-100 p-5">
curl -X GET \
'URL/api/version/' \
-H 'Accept: application/json' \
-u USEREMAIL + ':' + APIKEY
</pre>
</div>
</div>
</div>
63 changes: 63 additions & 0 deletions src/app/modules/profile/developer/developer.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { OnDestroy, OnInit } from '@angular/core'
import { ChangeDetectorRef, Component, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { MatButtonModule } from '@angular/material/button'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatIconModule } from '@angular/material/icon'
import { Subject, takeUntil } from 'rxjs'
import type { CurrentUser } from '@seed/api/user'
import { UserService } from '@seed/api/user'
import type { Alert } from '@seed/components'
import { AlertComponent } from '@seed/components'
import { SharedImports } from '@seed/directives'
@Component({
selector: 'seed-profile-developer',
templateUrl: './developer.component.html',
imports: [AlertComponent, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, ReactiveFormsModule, SharedImports],
})
export class ProfileDeveloperComponent implements OnInit, OnDestroy {
private _userService = inject(UserService)
private _changeDetectorRef = inject(ChangeDetectorRef)

alert: Alert
showAlert = false
user: CurrentUser

private readonly _unsubscribeAll$ = new Subject<void>()

ngOnInit(): void {
// Subscribe to user changes
this._userService.currentUser$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((currentUser) => {
this.user = currentUser

// Mark for check
this._changeDetectorRef.markForCheck()
})
}

ngOnDestroy(): void {
this._unsubscribeAll$.next()
this._unsubscribeAll$.complete()
}

generateKey(): void {
// send request to generate a new key; refresh user
this._userService.generateApiKey().subscribe({
error: (error) => {
console.error('Error:', error)
this.alert = {
type: 'error',
message: 'Generate New Key Unsuccessful...',
}
this.showAlert = true
},
complete: () => {
this.alert = {
type: 'success',
message: 'New API Key Generated!',
}
this.showAlert = true
},
})
}
}
51 changes: 51 additions & 0 deletions src/app/modules/profile/info/info.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<div class="flex-auto p-6 sm:p-10" *transloco="let t">
<div class="max-w-2xl">
<h2 class="mb-6 flex items-center text-center text-2xl">
<mat-icon class="mr-2" svgIcon="fa-solid:circle-user"></mat-icon>Profile Information
</h2>

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()" class="bg-card mt-8 flex flex-col overflow-hidden rounded-2xl p-8 pb-4 shadow">
<div class="gt-xs:flex-row flex flex-col">
<mat-form-field class="gt-xs:pr-3 flex-auto">
<mat-label>{{ t('First Name') }}</mat-label>
<input matInput [formControlName]="'firstName'" />
<mat-icon class="icon-size-5" matPrefix [svgIcon]="'fa-solid:circle-user'"></mat-icon>
@if (profileForm.controls['firstName']?.invalid) {
<mat-error>{{ t('Last Name required') }}</mat-error>
}
</mat-form-field>
<mat-form-field class="gt-xs:pl-3 flex-auto">
<mat-label>{{ t('Last Name') }}</mat-label>
<input matInput [formControlName]="'lastName'" />
<mat-icon class="icon-size-5" matPrefix [svgIcon]="'fa-solid:circle-user'"></mat-icon>
@if (profileForm.controls['lastName']?.invalid) {
<mat-error>{{ t('Last Name required') }}</mat-error>
}
</mat-form-field>
</div>
<div class="flex">
<mat-form-field class="w-full">
<mat-label>Email</mat-label>
<mat-icon class="icon-size-5" [svgIcon]="'fa-solid:envelope'" matPrefix></mat-icon>
<input [formControlName]="'email'" matInput />
@if (profileForm.controls['email']?.invalid) {
<mat-error>{{ t('Invalid email address') }}</mat-error>
}
</mat-form-field>
</div>
<div class="flex justify-center">
<button mat-flat-button color="primary" [disabled]="profileForm.invalid">
<span class="ml-2">{{ t('Update') }}</span>
</button>
</div>
<!-- Alert -->
@if (showAlert) {
<div class="mb-4 mt-0">
<seed-alert class="-mb-4 mt-8" appearance="outline" [showIcon]="false" [type]="alert.type">
{{ alert.message }}
</seed-alert>
</div>
}
</form>
</div>
</div>
Loading