Feature/add account registration page (#141)
* Add account registration page * Update changelog
This commit is contained in:
parent
aa4206af0e
commit
026a5011d4
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a dedicated page for the account registration
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Changed the buttons to links (`<a>`) on the tools page
|
- Changed the buttons to links (`<a>`) on the tools page
|
||||||
|
@ -45,6 +45,13 @@ const routes: Routes = [
|
|||||||
(m) => m.PricingPageModule
|
(m) => m.PricingPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'register',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/register/register-page.module').then(
|
||||||
|
(m) => m.RegisterPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'resources',
|
path: 'resources',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -55,7 +62,9 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'start',
|
path: 'start',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/login/login-page.module').then((m) => m.LoginPageModule)
|
import('./pages/landing/landing-page.module').then(
|
||||||
|
(m) => m.LandingPageModule
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tools',
|
path: 'tools',
|
||||||
|
@ -12,13 +12,15 @@
|
|||||||
<div *ngIf="canCreateAccount" class="container create-account-container">
|
<div *ngIf="canCreateAccount" class="container create-account-container">
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
<div class="col-md-6 offset-md-3">
|
<div class="col-md-6 offset-md-3">
|
||||||
<div
|
<a [routerLink]="['/']">
|
||||||
class="create-account-box p-2 text-center"
|
<mat-card
|
||||||
(click)="onCreateAccount()"
|
class="create-account-box p-2 text-center"
|
||||||
|
(click)="onCreateAccount()"
|
||||||
|
>
|
||||||
|
<div class="mt-1" i18n>You are using the Live Demo.</div>
|
||||||
|
<button mat-button color="primary" i18n>Create Account</button>
|
||||||
|
</mat-card></a
|
||||||
>
|
>
|
||||||
<div class="mt-1" i18n>You are using the Live Demo.</div>
|
|
||||||
<button mat-button color="primary" i18n>Create Account</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
padding: 5rem 0;
|
padding: 5rem 0;
|
||||||
|
|
||||||
.create-account-box {
|
.create-account-box {
|
||||||
border: 1px solid rgba(var(--palette-primary-500), 1);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
|
||||||
|
@ -68,17 +68,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
if (state?.user) {
|
this.user = state.user;
|
||||||
this.user = state.user;
|
|
||||||
|
|
||||||
this.canCreateAccount = hasPermission(
|
this.canCreateAccount = hasPermission(
|
||||||
this.user.permissions,
|
this.user?.permissions,
|
||||||
permissions.createUserAccount
|
permissions.createUserAccount
|
||||||
);
|
);
|
||||||
} else if (!this.tokenStorageService.getToken()) {
|
|
||||||
// User has not been logged in
|
|
||||||
this.user = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
@ -86,7 +81,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
public onCreateAccount() {
|
public onCreateAccount() {
|
||||||
this.tokenStorageService.signOut();
|
this.tokenStorageService.signOut();
|
||||||
window.location.reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSignOut() {
|
public onSignOut() {
|
||||||
|
@ -2,6 +2,7 @@ import { Platform } from '@angular/cdk/platform';
|
|||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import {
|
import {
|
||||||
DateAdapter,
|
DateAdapter,
|
||||||
MAT_DATE_FORMATS,
|
MAT_DATE_FORMATS,
|
||||||
@ -34,6 +35,7 @@ import { LanguageService } from './core/language.service';
|
|||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
MaterialCssVarsModule.forRoot({
|
MaterialCssVarsModule.forRoot({
|
||||||
darkThemeClass: 'is-dark-theme',
|
darkThemeClass: 'is-dark-theme',
|
||||||
isAutoContrast: true,
|
isAutoContrast: true,
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
class="d-none d-sm-block"
|
class="d-none d-sm-block"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="
|
[ngClass]="{
|
||||||
currentRoute === 'home' || currentRoute === 'zen' ? 'primary' : null
|
'font-weight-bold': currentRoute === 'home' || currentRoute === 'zen',
|
||||||
"
|
'text-decoration-underline':
|
||||||
|
currentRoute === 'home' || currentRoute === 'zen'
|
||||||
|
}"
|
||||||
[routerLink]="['/']"
|
[routerLink]="['/']"
|
||||||
>Overview</a
|
>Overview</a
|
||||||
>
|
>
|
||||||
@ -19,13 +21,16 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="
|
[ngClass]="{
|
||||||
currentRoute === 'analysis' ||
|
'font-weight-bold':
|
||||||
currentRoute === 'report' ||
|
currentRoute === 'analysis' ||
|
||||||
currentRoute === 'tools'
|
currentRoute === 'report' ||
|
||||||
? 'primary'
|
currentRoute === 'tools',
|
||||||
: null
|
'text-decoration-underline':
|
||||||
"
|
currentRoute === 'analysis' ||
|
||||||
|
currentRoute === 'report' ||
|
||||||
|
currentRoute === 'tools'
|
||||||
|
}"
|
||||||
[routerLink]="['/tools']"
|
[routerLink]="['/tools']"
|
||||||
>Tools</a
|
>Tools</a
|
||||||
>
|
>
|
||||||
@ -33,7 +38,10 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'transactions',
|
||||||
|
'text-decoration-underline': currentRoute === 'transactions'
|
||||||
|
}"
|
||||||
[routerLink]="['/transactions']"
|
[routerLink]="['/transactions']"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
@ -41,7 +49,10 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'accounts',
|
||||||
|
'text-decoration-underline': currentRoute === 'accounts'
|
||||||
|
}"
|
||||||
[routerLink]="['/accounts']"
|
[routerLink]="['/accounts']"
|
||||||
>Accounts</a
|
>Accounts</a
|
||||||
>
|
>
|
||||||
@ -50,7 +61,10 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'admin' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'admin',
|
||||||
|
'text-decoration-underline': currentRoute === 'admin'
|
||||||
|
}"
|
||||||
[routerLink]="['/admin']"
|
[routerLink]="['/admin']"
|
||||||
>Admin Control</a
|
>Admin Control</a
|
||||||
>
|
>
|
||||||
@ -58,7 +72,10 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'resources' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'resources',
|
||||||
|
'text-decoration-underline': currentRoute === 'resources'
|
||||||
|
}"
|
||||||
[routerLink]="['/resources']"
|
[routerLink]="['/resources']"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
@ -67,7 +84,10 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'pricing' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'pricing',
|
||||||
|
'text-decoration-underline': currentRoute === 'pricing'
|
||||||
|
}"
|
||||||
[routerLink]="['/pricing']"
|
[routerLink]="['/pricing']"
|
||||||
>Pricing</a
|
>Pricing</a
|
||||||
>
|
>
|
||||||
@ -75,7 +95,10 @@
|
|||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'about',
|
||||||
|
'text-decoration-underline': currentRoute === 'about'
|
||||||
|
}"
|
||||||
[routerLink]="['/about']"
|
[routerLink]="['/about']"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
@ -226,28 +249,44 @@
|
|||||||
<gf-logo></gf-logo>
|
<gf-logo></gf-logo>
|
||||||
</a>
|
</a>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<a
|
|
||||||
*ngIf="hasPermissionForSubscription"
|
|
||||||
i18n
|
|
||||||
mat-flat-button
|
|
||||||
[color]="currentRoute === 'pricing' ? 'primary' : null"
|
|
||||||
[routerLink]="['/pricing']"
|
|
||||||
>Pricing</a
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'about',
|
||||||
|
'text-decoration-underline': currentRoute === 'about'
|
||||||
|
}"
|
||||||
[routerLink]="['/about']"
|
[routerLink]="['/about']"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
*ngIf="hasPermissionForSubscription"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'pricing',
|
||||||
|
'text-decoration-underline': currentRoute === 'pricing'
|
||||||
|
}"
|
||||||
|
[routerLink]="['/pricing']"
|
||||||
|
>Pricing</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="d-none d-sm-block mx-1 no-min-width px-1"
|
||||||
href="https://github.com/ghostfolio/ghostfolio"
|
href="https://github.com/ghostfolio/ghostfolio"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
>GitHub</a
|
><ion-icon name="logo-github"></ion-icon
|
||||||
>
|
></a>
|
||||||
<button i18n mat-flat-button (click)="openLoginDialog()">Sign in</button>
|
<button class="mx-1" i18n mat-flat-button (click)="openLoginDialog()">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
class="d-none d-sm-block"
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[routerLink]="['/register']"
|
||||||
|
>Get Started
|
||||||
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
|
||||||
|
|
||||||
import { GfLogoModule } from '../logo/logo.module';
|
import { GfLogoModule } from '../logo/logo.module';
|
||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
|
@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
|||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'login-with-access-token-dialog',
|
selector: 'gf-login-with-access-token-dialog',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
styleUrls: ['./login-with-access-token-dialog.scss'],
|
styleUrls: ['./login-with-access-token-dialog.scss'],
|
||||||
templateUrl: 'login-with-access-token-dialog.html'
|
templateUrl: 'login-with-access-token-dialog.html'
|
@ -14,7 +14,12 @@ import { UserService } from '../services/user/user.service';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
private static PUBLIC_PAGE_ROUTES = ['/about', '/pricing', '/resources'];
|
private static PUBLIC_PAGE_ROUTES = [
|
||||||
|
'/about',
|
||||||
|
'/pricing',
|
||||||
|
'/register',
|
||||||
|
'/resources'
|
||||||
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
@ -2,14 +2,14 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
import { LoginPageComponent } from './login-page.component';
|
import { LandingPageComponent } from './landing-page.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: LoginPageComponent, canActivate: [AuthGuard] }
|
{ path: '', component: LandingPageComponent, canActivate: [AuthGuard] }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class LoginPageRoutingModule {}
|
export class LandingPageRoutingModule {}
|
@ -1,21 +1,17 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-login-page',
|
selector: 'gf-landing-page',
|
||||||
templateUrl: './login-page.html',
|
templateUrl: './landing-page.html',
|
||||||
styleUrls: ['./login-page.scss']
|
styleUrls: ['./landing-page.scss']
|
||||||
})
|
})
|
||||||
export class LoginPageComponent implements OnDestroy, OnInit {
|
export class LandingPageComponent implements OnDestroy, OnInit {
|
||||||
public currentYear = format(new Date(), 'yyyy');
|
public currentYear = format(new Date(), 'yyyy');
|
||||||
public demoAuthToken: string;
|
public demoAuthToken: string;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
@ -28,7 +24,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private tokenStorageService: TokenStorageService
|
private tokenStorageService: TokenStorageService
|
||||||
) {}
|
) {}
|
||||||
@ -46,15 +41,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createAccount() {
|
|
||||||
this.dataService
|
|
||||||
.postUser()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ accessToken, authToken }) => {
|
|
||||||
this.openShowAccessTokenDialog(accessToken, authToken);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public initializeLineChart() {
|
public initializeLineChart() {
|
||||||
this.historicalDataItems = [
|
this.historicalDataItems = [
|
||||||
{
|
{
|
||||||
@ -268,28 +254,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public openShowAccessTokenDialog(
|
|
||||||
accessToken: string,
|
|
||||||
authToken: string
|
|
||||||
): void {
|
|
||||||
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
|
|
||||||
data: {
|
|
||||||
accessToken,
|
|
||||||
authToken
|
|
||||||
},
|
|
||||||
disableClose: true,
|
|
||||||
width: '30rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data) => {
|
|
||||||
if (data?.authToken) {
|
|
||||||
this.tokenStorageService.saveToken(authToken);
|
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setToken(aToken: string) {
|
public setToken(aToken: string) {
|
||||||
this.tokenStorageService.saveToken(aToken);
|
this.tokenStorageService.saveToken(aToken);
|
||||||
|
|
@ -13,16 +13,16 @@
|
|||||||
class="align-items-center col d-flex justify-content-center position-relative"
|
class="align-items-center col d-flex justify-content-center position-relative"
|
||||||
>
|
>
|
||||||
<div class="py-5 text-center">
|
<div class="py-5 text-center">
|
||||||
<button
|
<a
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!demoAuthToken"
|
[disabled]="!demoAuthToken"
|
||||||
(click)="createAccount()"
|
[routerLink]="['/register']"
|
||||||
>
|
>
|
||||||
Create Account
|
Get Started
|
||||||
</button>
|
</a>
|
||||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
||||||
<button
|
<button
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
||||||
@ -135,15 +135,15 @@
|
|||||||
Join now or check out the example account
|
Join now or check out the example account
|
||||||
</p>
|
</p>
|
||||||
<div class="py-2 text-center">
|
<div class="py-2 text-center">
|
||||||
<button
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!demoAuthToken"
|
[disabled]="!demoAuthToken"
|
||||||
(click)="createAccount()"
|
[routerLink]="['/register']"
|
||||||
>
|
>
|
||||||
Create Account
|
Get Started
|
||||||
</button>
|
</a>
|
||||||
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
|
||||||
<button
|
<button
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
25
apps/client/src/app/pages/landing/landing-page.module.ts
Normal file
25
apps/client/src/app/pages/landing/landing-page.module.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
|
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
||||||
|
|
||||||
|
import { LandingPageRoutingModule } from './landing-page-routing.module';
|
||||||
|
import { LandingPageComponent } from './landing-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [LandingPageComponent],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfLineChartModule,
|
||||||
|
GfLogoModule,
|
||||||
|
LandingPageRoutingModule,
|
||||||
|
MatButtonModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class LandingPageModule {}
|
@ -1,7 +1,9 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Pricing Plans</h3>
|
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
||||||
|
Pricing Plans
|
||||||
|
</h3>
|
||||||
<mat-card class="mb-4">
|
<mat-card class="mb-4">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p>
|
<p>
|
||||||
@ -188,8 +190,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="!user" class="row">
|
<div *ngIf="!user" class="row">
|
||||||
<div class="col mt-3 text-center">
|
<div class="col mt-3 text-center">
|
||||||
<a color="primary" i18n mat-flat-button [routerLink]="['/start']">
|
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
||||||
Create Account
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
<p class="text-muted"><small>It's free</small></p>
|
<p class="text-muted"><small>It's free</small></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { RegisterPageComponent } from './register-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: RegisterPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class RegisterPageRoutingModule {}
|
@ -0,0 +1,98 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-register-page',
|
||||||
|
templateUrl: './register-page.html',
|
||||||
|
styleUrls: ['./register-page.scss']
|
||||||
|
})
|
||||||
|
export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||||
|
public currentYear = format(new Date(), 'yyyy');
|
||||||
|
public demoAuthToken: string;
|
||||||
|
public hasPermissionForSocialLogin: boolean;
|
||||||
|
public historicalDataItems: LineChartItem[];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private router: Router,
|
||||||
|
private tokenStorageService: TokenStorageService
|
||||||
|
) {
|
||||||
|
this.tokenStorageService.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.dataService
|
||||||
|
.fetchInfo()
|
||||||
|
.subscribe(({ demoAuthToken, globalPermissions }) => {
|
||||||
|
this.demoAuthToken = demoAuthToken;
|
||||||
|
this.hasPermissionForSocialLogin = hasPermission(
|
||||||
|
globalPermissions,
|
||||||
|
permissions.enableSocialLogin
|
||||||
|
);
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createAccount() {
|
||||||
|
this.dataService
|
||||||
|
.postUser()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ accessToken, authToken }) => {
|
||||||
|
this.openShowAccessTokenDialog(accessToken, authToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public openShowAccessTokenDialog(
|
||||||
|
accessToken: string,
|
||||||
|
authToken: string
|
||||||
|
): void {
|
||||||
|
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
|
||||||
|
data: {
|
||||||
|
accessToken,
|
||||||
|
authToken
|
||||||
|
},
|
||||||
|
disableClose: true,
|
||||||
|
width: '30rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data) => {
|
||||||
|
if (data?.authToken) {
|
||||||
|
this.tokenStorageService.saveToken(authToken);
|
||||||
|
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setToken(aToken: string) {
|
||||||
|
this.tokenStorageService.saveToken(aToken);
|
||||||
|
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
30
apps/client/src/app/pages/register/register-page.html
Normal file
30
apps/client/src/app/pages/register/register-page.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
|
||||||
|
Create your Ghostfolio account
|
||||||
|
</h3>
|
||||||
|
<mat-card class="mb-4">
|
||||||
|
<mat-card-content class="text-center">
|
||||||
|
<button
|
||||||
|
class="d-inline-block"
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[disabled]="!demoAuthToken"
|
||||||
|
(click)="createAccount()"
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="hasPermissionForSocialLogin">
|
||||||
|
<div class="my-3 text-muted" i18n>or</div>
|
||||||
|
<a color="accent" href="/api/auth/google" mat-flat-button
|
||||||
|
><ion-icon class="mr-1" name="logo-google"></ion-icon
|
||||||
|
><span i18n>Continue with Google</span></a
|
||||||
|
>
|
||||||
|
</ng-container>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,27 +1,27 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
|
||||||
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
||||||
|
|
||||||
import { LoginPageRoutingModule } from './login-page-routing.module';
|
import { RegisterPageRoutingModule } from './register-page-routing.module';
|
||||||
import { LoginPageComponent } from './login-page.component';
|
import { RegisterPageComponent } from './register-page.component';
|
||||||
import { ShowAccessTokenDialogModule } from './show-access-token-dialog/show-access-token-dialog.module';
|
import { ShowAccessTokenDialogModule } from './show-access-token-dialog/show-access-token-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [LoginPageComponent],
|
declarations: [RegisterPageComponent],
|
||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfLineChartModule,
|
|
||||||
GfLogoModule,
|
GfLogoModule,
|
||||||
LoginPageRoutingModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatCardModule,
|
||||||
|
RegisterPageRoutingModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
ShowAccessTokenDialogModule
|
ShowAccessTokenDialogModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class LoginPageModule {}
|
export class RegisterPageModule {}
|
3
apps/client/src/app/pages/register/register-page.scss
Normal file
3
apps/client/src/app/pages/register/register-page.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -7,7 +7,7 @@ import {
|
|||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'show-access-token-dialog',
|
selector: 'gf-show-access-token-dialog',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
styleUrls: ['./show-access-token-dialog.scss'],
|
styleUrls: ['./show-access-token-dialog.scss'],
|
||||||
templateUrl: 'show-access-token-dialog.html'
|
templateUrl: 'show-access-token-dialog.html'
|
@ -1,5 +1,5 @@
|
|||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field';
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
@ -1,12 +1,14 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { UserService } from './user/user.service';
|
||||||
|
|
||||||
const TOKEN_KEY = 'auth-token';
|
const TOKEN_KEY = 'auth-token';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class TokenStorageService {
|
export class TokenStorageService {
|
||||||
public constructor() {}
|
public constructor(private userService: UserService) {}
|
||||||
|
|
||||||
public getToken(): string {
|
public getToken(): string {
|
||||||
return window.localStorage.getItem(TOKEN_KEY);
|
return window.localStorage.getItem(TOKEN_KEY);
|
||||||
@ -22,6 +24,8 @@ export class TokenStorageService {
|
|||||||
|
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
|
this.userService.remove();
|
||||||
|
|
||||||
if (utmSource) {
|
if (utmSource) {
|
||||||
window.localStorage.setItem('utm_source', utmSource);
|
window.localStorage.setItem('utm_source', utmSource);
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,22 @@
|
|||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io</loc>
|
<loc>https://ghostfol.io</loc>
|
||||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/about</loc>
|
<loc>https://ghostfol.io/about</loc>
|
||||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pricing</loc>
|
<loc>https://ghostfol.io/pricing</loc>
|
||||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/register</loc>
|
||||||
|
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/resources</loc>
|
<loc>https://ghostfol.io/resources</loc>
|
||||||
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
@ -153,3 +153,7 @@ ngx-skeleton-loader {
|
|||||||
.no-min-width {
|
.no-min-width {
|
||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-decoration-underline {
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user