Skip to content

Commit

Permalink
[Feat] Implement magic login (#8387)
Browse files Browse the repository at this point in the history
* feat: create reusable workspace selection component

* feat: create debounce click directive

* feat: create reusable avatar component

* feat: implement login magic component

* feat: implement login magic workspace component

* feat: implement login workspace component

* feat: reuse shared core styles

* feat: add new component to login

* feat: create authentication routes

* feat: use lazy loading for authentications routes

* fix: add code rabbit suggestions

* fix: apply code rabbit suggestions

* fix: apply code rabbit suggestions
  • Loading branch information
adkif authored Oct 13, 2024
1 parent 6721cfc commit f8080cf
Show file tree
Hide file tree
Showing 23 changed files with 2,110 additions and 36 deletions.
5 changes: 4 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,10 @@
"tsConfig": "apps/desktop-timer/tsconfig.app.json",
"aot": true,
"stylePreprocessorOptions": {
"includePaths": ["apps/desktop-timer/src/assets/styles"]
"includePaths": [
"apps/desktop-timer/src/assets/styles",
"packages/ui-core/static/styles"
]
},
"assets": [
"apps/desktop-timer/src/favicon.ico",
Expand Down
28 changes: 2 additions & 26 deletions apps/desktop-timer/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
AlwaysOnComponent,
AuthGuard,
ImageViewerComponent,
NgxLoginComponent,
NoAuthGuard,
ScreenCaptureComponent,
ServerDownPage,
SettingsComponent,
Expand All @@ -15,7 +13,6 @@ import {
TimeTrackerComponent,
UpdaterComponent
} from '@gauzy/desktop-ui-lib';
import { NbAuthComponent, NbRequestPasswordComponent, NbResetPasswordComponent } from '@nebular/auth';
import { AppModuleGuard } from './app.module.guards';

const routes: Routes = [
Expand All @@ -25,29 +22,8 @@ const routes: Routes = [
},
{
path: 'auth',
component: NbAuthComponent,
children: [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: NgxLoginComponent,
canActivate: [AppModuleGuard, NoAuthGuard]
},
{
path: 'request-password',
component: NbRequestPasswordComponent,
canActivate: [AppModuleGuard, NoAuthGuard]
},
{
path: 'reset-password',
component: NbResetPasswordComponent,
canActivate: [AppModuleGuard, NoAuthGuard]
}
]
loadChildren: () => import('@gauzy/desktop-ui-lib').then((m) => m.authRoutes),
canActivate: [AppModuleGuard]
},
{
path: 'time-tracker',
Expand Down
75 changes: 75 additions & 0 deletions packages/desktop-ui-lib/src/lib/auth/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Route } from '@angular/router';
import {
NbAuthComponent,
NbLogoutComponent,
NbRegisterComponent,
NbRequestPasswordComponent,
NbResetPasswordComponent
} from '@nebular/auth';
import { NgxLoginComponent } from '../login';
import { NgxLoginMagicComponent } from '../login/features/login-magic/login-magic.component';
import { NgxLoginWorkspaceComponent } from '../login/features/login-workspace/login-workspace.component';
import { NgxMagicSignInWorkspaceComponent } from '../login/features/magic-login-workspace/magic-login-workspace.component';
import { NoAuthGuard } from './no-auth.guard';

export const authRoutes: Route[] = [
{
path: '',
component: NbAuthComponent,
children: [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: NgxLoginComponent,
canActivate: [NoAuthGuard]
},
{
path: 'register',
component: NbRegisterComponent,
canActivate: [NoAuthGuard]
},
{
path: 'logout',
component: NbLogoutComponent
},
{
path: 'request-password',
component: NbRequestPasswordComponent,
canActivate: [NoAuthGuard]
},
{
path: 'reset-password',
component: NbResetPasswordComponent,
canActivate: [NoAuthGuard]
},
{
// Register the path 'login-workspace'
path: 'login-workspace',
// Register the component to load component: NgxLoginWorkspaceComponent,
component: NgxLoginWorkspaceComponent,
// Register the data object
canActivate: [NoAuthGuard]
},
{
// Register the path 'login-magic'
path: 'login-magic',
// Register the component to load component: NgxLoginMagicComponent,
component: NgxLoginMagicComponent,
// Register the data object
canActivate: [NoAuthGuard]
},
{
// Register the path 'magic-sign-in'
path: 'magic-sign-in',
// Register the component to load component: NgxMagicSignInWorkspaceComponent,
component: NgxMagicSignInWorkspaceComponent,
// Register the data object
canActivate: [NoAuthGuard]
}
]
}
];
4 changes: 2 additions & 2 deletions packages/desktop-ui-lib/src/lib/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './services';
export * from './auth.guard';
export * from './no-auth.guard';
export * from './auth.module';
export * from './auth.routes';
export * from './no-auth.guard';
export * from './services';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Directive, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subject, Subscription, tap } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

@Directive({
selector: '[debounceClick]'
})
export class DebounceClickDirective implements OnInit, OnDestroy {
private clicks: Subject<Event> = new Subject<Event>();
private subscription: Subscription;

@Input() debounceTime = 300;
@Output() throttledClick: EventEmitter<Event> = new EventEmitter<Event>();

/**
* Handles the click event and emits it after a debounce time.
*
* @param {Event} event - The click event object.
* @return {void} This function does not return a value.
*/
@HostListener('click', ['$event'])
clickEvent(event: Event): void {
this.clicks.next(event);
}

ngOnInit() {
this.subscription = this.clicks
.pipe(
debounceTime(this.debounceTime),
tap((e) => this.throttledClick.emit(e))
)
.subscribe();
}

ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NbSpinnerModule } from '@nebular/theme';
import { DebounceClickDirective } from './debounce-click.directive';
import { DynamicDirective } from './dynamic.directive';
import { SpinnerButtonDirective } from './spinner-button.directive';
import { TextMaskDirective } from './text-mask.directive';

@NgModule({
declarations: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective],
exports: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective],
declarations: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective, DebounceClickDirective],
exports: [SpinnerButtonDirective, DynamicDirective, TextMaskDirective, DebounceClickDirective],
imports: [CommonModule, NbSpinnerModule]
})
export class DesktopDirectiveModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<section class="login-container">
<div class="login-wrapper">
<div class="svg-wrapper">
<gauzy-logo></gauzy-logo>
<gauzy-switch-theme></gauzy-switch-theme>
</div>
<div class="headings" [class]="isDemo ? 'headings-demo' : ''">
<div class="headings-inner">
<h2 id="title" class="title">{{ 'LOGIN_PAGE.TITLE' | translate }}</h2>
<p class="sub-title">{{ 'LOGIN_PAGE.LOGIN_MAGIC.TITLE' | translate }}</p>
</div>
<ng-template [ngIf]="isCodeSent">
<div class="sent-code-container">
<p
[ngClass]="{
'normal-text': email?.value.length < 30,
'minimum-text': email?.value.length >= 30
}"
>
{{ 'LOGIN_PAGE.LOGIN_MAGIC.SUCCESS_SENT_CODE_TITLE' | translate }}
<b class="title">{{ email?.value }}</b>
<br />
<span>{{ 'LOGIN_PAGE.LOGIN_MAGIC.SUCCESS_SENT_CODE_SUB_TITLE' | translate }}</span>
</p>
</div>
</ng-template>
</div>
<div class="hr-div-strong"></div>
<!-- -->
<form #formDirective="ngForm" [formGroup]="form" (ngSubmit)="confirmSignInCode()">
<!-- Email input -->
<div class="form-control-group">
<label class="label" for="input-email">{{ 'LOGIN_PAGE.LABELS.EMAIL' | translate }}</label>
<nb-form-field>
<input
name="input-email"
id="input-email"
nbInput
fullWidth
formControlName="email"
[placeholder]="'LOGIN_PAGE.PLACEHOLDERS.EMAIL' | translate"
[status]="email.dirty ? (email.invalid ? 'danger' : 'success') : 'basic'"
[attr.aria-invalid]="email.invalid && email.touched ? true : null"
autofocus
autocomplete="off"
[ngClass]="isCodeSent ? 'not-allowed' : ''"
/>
<nb-icon
class="edit-email"
*ngIf="isCodeSent"
nbSuffix
nbButton
size="small"
ghost
icon="edit-outline"
nbTooltip="edit email"
nbTooltipPosition="top"
></nb-icon>
</nb-form-field>
<ng-container *ngIf="email.invalid && email.touched && !email.pristine">
<p class="caption status-danger" *ngIf="email?.errors?.required">
{{ 'LOGIN_PAGE.VALIDATION.EMAIL_REQUIRED' | translate }}
</p>
<p class="caption status-danger" *ngIf="email?.errors?.pattern">
{{ 'LOGIN_PAGE.VALIDATION.EMAIL_REAL_REQUIRED' | translate }}
</p>
</ng-container>
</div>
<!-- Code input -->
<ng-container *ngIf="isCodeSent">
<div class="form-control-group">
<label class="label" for="input-code">{{ 'LOGIN_PAGE.LABELS.CODE' | translate }}</label>
<input
name="input-code"
id="input-code"
nbInput
fullWidth
formControlName="code"
noSpaceEdges
[placeholder]="'LOGIN_PAGE.PLACEHOLDERS.CODE' | translate"
[status]="code.dirty ? (code.invalid ? 'danger' : 'success') : 'basic'"
[attr.aria-invalid]="code.invalid && code.touched ? true : null"
maxlength="6"
autofocus
autocomplete="off"
/>
<ng-container *ngIf="code.invalid && code.touched">
<p class="caption status-danger" *ngIf="code.errors?.required">
{{ 'LOGIN_PAGE.VALIDATION.CODE_REQUIRED' | translate }}
</p>
<p class="caption status-danger" *ngIf="code.errors?.minlength">
{{
'LOGIN_PAGE.VALIDATION.CODE_REQUIRED_LENGTH'
| translate : { requiredLength: code.errors?.minlength?.requiredLength }
}}
</p>
</ng-container>
<!-- Resend Code Button & Text -->
<ng-template [ngIf]="isCodeSent">
<p class="new-code-wrapper">
<ng-template [ngIf]="isCodeResent" [ngIfElse]="resendButton">
<span class="request-new-code">
{{
'LOGIN_PAGE.LOGIN_MAGIC.REQUEST_NEW_CODE_TITLE'
| translate : { countdown: countdown }
}}
</span>
</ng-template>

<ng-template #resendButton>
<a class="resend-code" debounceClick (throttledClick)="onResendCode()">
{{ 'LOGIN_PAGE.LOGIN_MAGIC.RESEND_CODE_TITLE' | translate }}
</a>
</ng-template>
</p>
</ng-template>
</div>
</ng-container>
<!-- Submit Button -->
<div class="submit-btn-wrapper">
<a class="forgot-email caption-2 forgot-email-big" href="mailto:forgot@gauzy.co">
{{ 'LOGIN_PAGE.FORGOT_EMAIL_TITLE' | translate }}
</a>
<div class="submit-inner-wrapper">
<ng-template [ngIf]="isCodeSent" [ngIfElse]="sendCodeButtonTemplate">
<button
type="submit"
nbButton
size="tiny"
class="submit-btn"
[disabled]="form.invalid || isLoading"
>
<span class="btn-text">
{{ 'BUTTONS.LOGIN' | translate }}
</span>
<ng-template [ngIf]="isLoading">
<nb-icon [ngStyle]="{ display: 'none' }" *gauzySpinnerButton="isLoading"></nb-icon>
</ng-template>
</button>
</ng-template>
<ng-template #sendCodeButtonTemplate>
<button
type="button"
nbButton
size="tiny"
class="submit-btn"
[disabled]="email.invalid || isLoading"
(click)="sendLoginCode()"
>
<span class="btn-text">
{{ 'BUTTONS.SEND_CODE' | translate }}
</span>
<ng-template [ngIf]="isLoading">
<nb-icon [ngStyle]="{ display: 'none' }" *gauzySpinnerButton="isLoading"></nb-icon>
</ng-template>
</button>
</ng-template>
</div>
</div>
<div class="magic-description">
<p class="sub-title">
{{ 'LOGIN_PAGE.LOGIN_MAGIC.DESCRIPTION_TITLE' | translate }}
<span class="sub-title">
<a routerLink="/auth/login">
{{ 'LOGIN_PAGE.LOGIN_MAGIC.OR_SIGN_IN_WITH_PASSWORD' | translate }}
</a>
</span>
</p>
</div>
</form>
<div class="hr-div-soft"></div>
<section>
<ngx-social-links></ngx-social-links>
</section>
<div class="hr-div-soft"></div>
<section class="another-action" aria-label="Register">
{{ 'LOGIN_PAGE.DO_NOT_HAVE_ACCOUNT_TITLE' | translate }}
<a class="text-link" routerLink="/auth/register">
{{ 'BUTTONS.REGISTER' | translate }}
</a>
</section>
</div>
</section>
Loading

0 comments on commit f8080cf

Please sign in to comment.