add angular18 boilerplate

This commit is contained in:
Tykayn 2025-10-15 11:17:29 +02:00 committed by tykayn
parent ab962e27d3
commit 7cebea45d8
122 changed files with 15816 additions and 0 deletions

View file

@ -0,0 +1,9 @@
<div class="scrollable-container">
<div class="layout">
<router-outlet></router-outlet>
</div>
</div>
<app-toast aria-live="polite" aria-atomic="true" *ngIf="!storeService.isServer()"></app-toast>

View file

@ -0,0 +1,9 @@
.scrollable-container {
height: 100vh;
display: block;
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
}

View file

@ -0,0 +1,56 @@
// Angular modules
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
// Services
import { StoreService } from '@services/store.service';
// Components
import { ToastComponent } from '@blocks/toast/toast.component';
@Component({
selector : 'app-root',
templateUrl : './app.component.html',
styleUrls : ['./app.component.scss'],
standalone : true,
imports : [RouterOutlet, ToastComponent, NgIf]
})
export class AppComponent implements OnInit
{
constructor
(
public storeService : StoreService,
)
{
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
public ngOnInit() : void
{
}
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Computed props -----------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Subscriptions ------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,84 @@
// Angular modules
import { DatePipe } from '@angular/common';
import { withFetch } from '@angular/common/http';
import { withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClient } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { importProvidersFrom } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { withInMemoryScrolling } from '@angular/router';
import { withRouterConfig } from '@angular/router';
// External modules
import { TranslateModule } from '@ngx-translate/core';
import { TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AngularSvgIconModule } from 'angular-svg-icon';
// Internal modules
import { environment } from '@env/environment';
import { routes } from './app.routes';
// Services
import { AppService } from '@services/app.service';
import { StoreService } from '@services/store.service';
export function createTranslateLoader(http : HttpClient)
{
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig : ApplicationConfig = {
providers : [
// Routing
provideRouter(
routes,
withRouterConfig({
onSameUrlNavigation : 'reload',
}),
withInMemoryScrolling({
scrollPositionRestoration : 'enabled'
}),
),
importProvidersFrom(
// Angular modules
BrowserModule,
// External modules
TranslateModule.forRoot({
defaultLanguage : environment.defaultLanguage,
loader : {
provide : TranslateLoader,
useFactory : (createTranslateLoader),
deps : [HttpClient]
}
}),
AngularSvgIconModule.forRoot(),
// Internal modules
),
// External modules
// Services
StoreService,
AppService,
// Pipes
DatePipe,
// Guards
// Resolvers
provideHttpClient(withFetch(), withInterceptorsFromDi()),
provideAnimations(),
provideClientHydration(),
]
};

View file

@ -0,0 +1,18 @@
// Angular modules
import { Routes } from '@angular/router';
export const routes : Routes = [
{
path : 'auth',
loadChildren : () => import('./pages/auth/auth.routes').then(m => m.routes),
},
{
path : 'home',
loadComponent : () => import('./pages/home/home.component').then(m => m.HomeComponent),
},
{ path : '', redirectTo : '/home', pathMatch : 'full' },
{
path : '**',
loadComponent : () => import('./pages/not-found/not-found.component').then(m => m.NotFoundComponent),
},
];

View file

@ -0,0 +1,21 @@
<!-- NOTE Header with Project logo -->
<header class="position-fixed w-100 text-center py-2 bg-light">
<img src="./assets/img/project/logo.svg" [alt]="appName + ' Logo'" id="auth-svg" class="img-fluid"/>
</header>
<!-- NOTE Loading -->
<app-progress-bar *ngIf="storeService.isLoading$ | async" class="position-fixed w-100"></app-progress-bar>
<div class="auth-component-container">
<div class="auth-component">
<!-- NOTE Form -->
<router-outlet></router-outlet>
</div>
</div>
<!-- NOTE Footer with Application version -->
<footer class="position-fixed w-100 text-white text-end px-2 d-flex align-items-center justify-content-end bg-primary">
<div class="small">{{ 'VERSION' | translate }} {{ appVersion }}</div>
</footer>

View file

@ -0,0 +1,41 @@
// NOTE Header / Loading / Footer
header, app-progress-bar, footer {
z-index: 10;
}
header {
top: 0;
height: 56px;
}
app-progress-bar {
top: 56px;
}
footer {
bottom: 0;
height: 40px;
}
// NOTE Layout
.auth-component-container {
display: flex;
align-items: center;
min-height: 100vh;
padding-top: 60px;
padding-bottom: 40px;
.auth-component {
width: 100%;
max-width: 350px;
padding: 15px;
margin: auto;
}
}
// NOTE Images
#auth-svg {
height: 40px;
}

View file

@ -0,0 +1,10 @@
<app-progress-bar class="position-fixed w-100" *ngIf="storeService.isLoading$ | async"></app-progress-bar>
<div class="row g-0">
<div class="col img-wrapper d-none d-md-flex">
<img [src]="'assets/img/project/login.png'" alt="Login image">
</div>
<div class="col d-flex flex-column justify-content-center p-3">
<!-- NOTE Content -->
<router-outlet></router-outlet>
</div>
</div>

View file

@ -0,0 +1,14 @@
// NOTE Layout
.row {
min-height: 100vh;
.col.img-wrapper {
min-height: 100vh;
overflow: hidden;
img {
object-fit: cover;
width: 100%;
height: 100%;
position: relative;
}
}
}

View file

@ -0,0 +1,66 @@
// Angular modules
import { NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
// Internal modules
import { environment } from '@env/environment';
// Services
import { StoreService } from '@services/store.service';
// Components
import { ProgressBarComponent } from '@blocks/progress-bar/progress-bar.component';
@Component({
selector : 'app-auth',
templateUrl : './auth.component.html',
styleUrls : ['./auth.component.scss'],
standalone : true,
imports : [NgIf, ProgressBarComponent, RouterOutlet, AsyncPipe]
})
export class AuthComponent implements OnInit
{
// NOTE Component properties
public appName : string = environment.appName;
public appVersion : string = environment.version;
constructor
(
public storeService : StoreService,
)
{
}
public ngOnInit() : void
{
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Computed props -----------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Subscriptions ------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,27 @@
// Angular modules
import { Routes } from '@angular/router';
export const routes : Routes = [
{
path : '',
children : [
{
path : '',
redirectTo : 'login',
pathMatch : 'full',
},
{
path : 'login',
loadComponent : () => import('./login/login.component').then(m => m.LoginComponent),
},
{
path : 'forgot-password',
loadComponent : () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent),
},
{
path : 'validate-account',
loadComponent : () => import('./validate-account/validate-account.component').then(m => m.ValidateAccountComponent),
},
]
}
];

View file

@ -0,0 +1,38 @@
<div class="text-center text-primary mb-4">
<div class="h2 fw-normal mb-0">{{ 'FORGOT_YOUR_PWD' | translate }}</div>
</div>
<div class="d-flex justify-content-center">
<div class="wrapper">
<!-- NOTE Login form -->
<form [formGroup]="formGroup" (ngSubmit)="onClickSubmit()">
<!-- NOTE Email -->
<div class="mb-3">
<label for="email" class="form-label text-start w-100">{{ 'EMAIL' | translate }}</label>
<input type="email" class="form-control" id="email"
formControlName="email"
[ngClass]="{ 'is-invalid' : formGroup.controls.email.errors && formGroup.controls.email.touched }">
<!-- NOTE Errors -->
<div class="invalid-feedback" *ngIf="formGroup.controls.email.hasError('required')">
{{ 'FIELD_REQUIRED' | translate }}
</div>
<div class="invalid-feedback" *ngIf="formGroup.controls.email.hasError('email')">
{{ 'FIELD_EMAIL' | translate }}
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary px-5" [disabled]="!formGroup.valid">
{{ 'RESET_PASSWORD' | translate }}
</button>
</div>
<div class="text-center my-3">
<a routerLink="/auth/login">{{ 'BACK_TO_LOGIN' | translate }}</a>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,4 @@
.wrapper {
max-width: 350px;
width: 100%;
}

View file

@ -0,0 +1,89 @@
// Angular modules
import { NgClass } from '@angular/common';
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { RouterLink } from '@angular/router';
// External modules
import { TranslateModule } from '@ngx-translate/core';
// Services
import { AppService } from '@services/app.service';
import { StoreService } from '@services/store.service';
@Component({
selector : 'app-forgot-password',
templateUrl : './forgot-password.component.html',
styleUrls : ['./forgot-password.component.scss'],
standalone : true,
imports : [FormsModule, ReactiveFormsModule, NgClass, NgIf, RouterLink, TranslateModule]
})
export class ForgotPasswordComponent
{
public formGroup !: FormGroup<{
email : FormControl<string>
}>;
constructor
(
public router : Router,
private storeService : StoreService,
private appService : AppService,
)
{
this.initFormGroup();
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
private initFormGroup() : void
{
this.formGroup = new FormGroup({
email : new FormControl<string>({
value : '',
disabled : false
}, { validators : [Validators.required, Validators.email], nonNullable : true }),
});
}
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
public async onClickSubmit() : Promise<void>
{
await this.forgotPassword();
}
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
private async forgotPassword() : Promise<void>
{
this.storeService.isLoading.set(true);
const email = this.formGroup.controls.email.getRawValue();
const success = await this.appService.forgotPassword(email);
this.storeService.isLoading.set(false);
if (!success)
return;
// NOTE Redirect to validate account
this.router.navigate(['/auth/validate-account']);
}
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,52 @@
<div class="text-center text-primary mb-4">
<div class="h2 fw-normal mb-0">{{ 'WELCOME_TO' | translate }}</div>
<div class="h1 fw-bold text-uppercase">{{ appName }}</div>
<div class="fw-normal text-black">{{ 'PROJECT_DESC' | translate }}</div>
</div>
<div class="d-flex justify-content-center">
<div class="wrapper">
<!-- NOTE Login form -->
<form [formGroup]="formGroup" (ngSubmit)="onClickSubmit()">
<!-- NOTE Email -->
<div class="mb-3">
<label for="email" class="form-label text-start w-100">{{ 'EMAIL' | translate }}</label>
<input type="email" class="form-control" id="email"
formControlName="email"
[ngClass]="{ 'is-invalid' : formGroup.controls.email.errors && formGroup.controls.email.touched }">
<!-- NOTE Errors -->
<div class="invalid-feedback" *ngIf="formGroup.controls.email.hasError('required')">
{{ 'FIELD_REQUIRED' | translate }}
</div>
<div class="invalid-feedback" *ngIf="formGroup.controls.email.hasError('email')">
{{ 'FIELD_EMAIL' | translate }}
</div>
</div>
<!-- NOTE Password -->
<div class="mb-3">
<label for="password" class="form-label text-start w-100">{{ 'PASSWORD' | translate }}</label>
<input type="password" class="form-control" id="password"
formControlName="password"
[ngClass]="{ 'is-invalid' : formGroup.controls.password.errors && formGroup.controls.password.touched }">
<!-- NOTE Errors -->
<div class="invalid-feedback" *ngIf="formGroup.controls.password.hasError('required')">
{{ 'FIELD_REQUIRED' | translate }}
</div>
</div>
<div class="text-end mb-3">
<a routerLink="/auth/forgot-password" class="">{{ 'FORGOT_YOUR_PWD' | translate }}</a>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary mb-3 px-5" [disabled]="!formGroup.valid">
<span class="px-4 d-block">{{ 'SIGN_IN' | translate }}</span>
</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,4 @@
.wrapper {
max-width: 350px;
width: 100%;
}

View file

@ -0,0 +1,100 @@
// Angular modules
import { NgClass } from '@angular/common';
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { RouterLink } from '@angular/router';
// External modules
import { TranslateModule } from '@ngx-translate/core';
// Internal modules
import { environment } from '@env/environment';
// Services
import { AppService } from '@services/app.service';
import { StoreService } from '@services/store.service';
@Component({
selector : 'app-login',
templateUrl : './login.component.html',
styleUrls : ['./login.component.scss'],
standalone : true,
imports : [FormsModule, ReactiveFormsModule, NgClass, NgIf, RouterLink, TranslateModule]
})
export class LoginComponent
{
public appName : string = environment.appName;
public formGroup !: FormGroup<{
email : FormControl<string>,
password : FormControl<string>,
}>;
constructor
(
private router : Router,
private storeService : StoreService,
private appService : AppService,
)
{
this.initFormGroup();
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
private initFormGroup() : void
{
this.formGroup = new FormGroup({
email : new FormControl<string>({
value : '',
disabled : false
}, { validators : [Validators.required, Validators.email], nonNullable : true }),
password : new FormControl<string>({
value : '',
disabled : false
}, { validators : [Validators.required], nonNullable : true })
});
}
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
public async onClickSubmit() : Promise<void>
{
await this.authenticate();
}
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
private async authenticate() : Promise<void>
{
this.storeService.isLoading.set(true);
const email = this.formGroup.controls.email.getRawValue();
const password = this.formGroup.controls.password.getRawValue();
const success = await this.appService.authenticate(email, password);
this.storeService.isLoading.set(false);
if (!success)
return;
// NOTE Redirect to home
this.router.navigate(['/home']);
}
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,31 @@
<div class="text-center text-primary mb-4">
<div class="h2 fw-normal mb-0">{{ 'VALIDATE_ACCOUNT' | translate }}</div>
</div>
<div class="d-flex justify-content-center">
<div class="wrapper">
<!-- NOTE Login form -->
<form [formGroup]="formGroup" (ngSubmit)="onClickSubmit()">
<!-- NOTE Password -->
<div class="mb-3">
<label for="password" class="form-label text-start w-100">{{ 'PASSWORD' | translate }}</label>
<input type="password" class="form-control" id="password"
formControlName="password"
[ngClass]="{ 'is-invalid' : formGroup.controls.password.errors && formGroup.controls.password.touched }">
<!-- NOTE Errors -->
<div class="invalid-feedback" *ngIf="formGroup.controls.password.hasError('required')">
{{ 'FIELD_REQUIRED' | translate }}
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary px-5" [disabled]="!formGroup.valid">
{{ 'VALIDATE' | translate }}
</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,4 @@
.wrapper {
max-width: 350px;
width: 100%;
}

View file

@ -0,0 +1,111 @@
// Angular modules
import { NgClass } from '@angular/common';
import { NgIf } from '@angular/common';
import { OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Router } from '@angular/router';
import { Params } from '@angular/router';
// External modules
import { TranslateModule } from '@ngx-translate/core';
// Internal modules
import { environment } from '@env/environment';
// Services
import { AppService } from '@services/app.service';
import { StoreService } from '@services/store.service';
@Component({
selector : 'app-validate-account',
templateUrl : './validate-account.component.html',
styleUrls : ['./validate-account.component.scss'],
standalone : true,
imports : [FormsModule, ReactiveFormsModule, NgClass, NgIf, TranslateModule]
})
export class ValidateAccountComponent implements OnInit
{
public formGroup !: FormGroup<{
password : FormControl<string>
}>;
private tokenFromUrl : string = '';
constructor
(
private router : Router,
private storeService : StoreService,
private activatedRoute : ActivatedRoute,
private appService : AppService,
)
{
this.initFormGroup();
}
public async ngOnInit() : Promise<void>
{
// NOTE Get token from URL
this.activatedRoute.queryParams.subscribe((params : Params) =>
{
this.tokenFromUrl = params['token'];
if (!environment.production)
console.log('ValidateAccountComponent : ngOnInit -> Token : ', this.tokenFromUrl);
});
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
private initFormGroup() : void
{
this.formGroup = new FormGroup({
password : new FormControl<string>({
value : '',
disabled : false
}, { validators : [Validators.required], nonNullable : true }),
});
}
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
public async onClickSubmit() : Promise<void>
{
if (!this.tokenFromUrl)
return;
await this.validateNewAccount();
}
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
private async validateNewAccount() : Promise<void>
{
this.storeService.isLoading.set(true);
const password = this.formGroup.controls.password.getRawValue();
const success = await this.appService.validateAccount(this.tokenFromUrl, password);
this.storeService.isLoading.set(false);
if (!success)
return;
// NOTE Redirect to home
this.router.navigate(['/home']);
}
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,13 @@
<app-page-layout>
<div class="container my-3">
<app-progress-bar *ngIf="storeService.isLoading()"></app-progress-bar>
<ng-container *ngIf="!storeService.isLoading()">
<div class="text-center">
Let's start the project!
</div>
</ng-container>
</div>
</app-page-layout>

View file

@ -0,0 +1,60 @@
// Angular modules
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
// Services
import { StoreService } from '@services/store.service';
// Components
import { ProgressBarComponent } from '@blocks/progress-bar/progress-bar.component';
import { PageLayoutComponent } from '@layouts/page-layout/page-layout.component';
@Component({
selector : 'app-home',
templateUrl : './home.component.html',
styleUrls : ['./home.component.scss'],
standalone : true,
imports : [PageLayoutComponent, NgIf, ProgressBarComponent]
})
export class HomeComponent implements OnInit
{
constructor
(
public storeService : StoreService
)
{ }
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
public ngOnInit() : void
{
setTimeout(_ =>
{
this.storeService.isLoading.set(false);
}, 2000);
}
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Computed props -----------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Subscriptions ------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,8 @@
<div class="not-found-container">
<div class="not-found text-center">
<h1 class="mb-3">{{ 'NOT_FOUND' | translate }}</h1>
<a class="btn btn-lg btn-primary" routerLink="/home">
{{ 'GO_TO_HOMEPAGE' | translate }}
</a>
</div>
</div>

View file

@ -0,0 +1,9 @@
.not-found-container {
display: flex;
align-items: center;
min-height: 100vh;
position: relative;
.not-found {
width: 100%;
}
}

View file

@ -0,0 +1,42 @@
// Angular modules
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
// External modules
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector : 'app-not-found',
templateUrl : './not-found.component.html',
styleUrls : ['./not-found.component.scss'],
standalone : true,
imports : [RouterLink, TranslateModule]
})
export class NotFoundComponent
{
constructor() { }
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Computed props -----------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Subscriptions ------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,10 @@
<!-- How to write CSS keyframes to indeterminate material design progress bar -->
<!-- https://stackoverflow.com/a/34773398/7462178 -->
<div class="slider">
<div class="line"></div>
<div class="subline inc"></div>
<div class="subline dec"></div>
</div>
<div class="text-center my-3" *ngIf="withLabel">{{ 'LOADING' | translate }}...</div>

View file

@ -0,0 +1,33 @@
.slider {
position: relative;
width: 100%;
height: 5px;
overflow-x: hidden;
.line {
position: absolute;
opacity: 0.4;
background: #4a8df8;
width: 150%;
height: 5px;
}
.subline {
position: absolute;
background: #4a8df8;
height: 5px;
&.inc {
animation: increase 2s infinite;
}
&.dec {
animation: decrease 2s 0.5s infinite;
}
}
}
@keyframes increase {
from { left: -5%; width: 5%; }
to { left: 130%; width: 100%;}
}
@keyframes decrease {
from { left: -80%; width: 80%; }
to { left: 110%; width: 10%;}
}

View file

@ -0,0 +1,27 @@
// Angular modules
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { Input } from '@angular/core';
import { OnInit } from '@angular/core';
// External modules
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector : 'app-progress-bar',
templateUrl : './progress-bar.component.html',
styleUrls : ['./progress-bar.component.scss'],
standalone : true,
imports : [NgIf, TranslateModule]
})
export class ProgressBarComponent implements OnInit
{
@Input() withLabel : boolean = false;
constructor() { }
public ngOnInit() : void
{
}
}

View file

@ -0,0 +1,31 @@
<ng-container *ngFor="let toast of toastManager.toasts">
<!-- NOTE With header -->
<ng-container *ngIf="toast.headerKey && toast.withHeader else withoutHeader">
<ngb-toast class="bg-{{ toast.type }}"
[autohide]="toast.autoHide"
[delay]="toast.delay"
(hide)="toastManager.remove(toast.id)">
<ng-template ngbToastHeader>
<!-- TODO Icon : Success / Info / Error -->
<div class="text-white me-auto">{{ toast.headerKey | translate }}</div>
</ng-template>
<div class="text-dark">{{ toast.body }}</div>
</ngb-toast>
</ng-container>
<!-- NOTE Without header -->
<ng-template #withoutHeader>
<ngb-toast class="bg-{{ toast.type }}"
[autohide]="toast.autoHide"
[delay]="toast.delay"
(hide)="toastManager.remove(toast.id)">
<div class="text-dark">{{ toast.body }}</div>
</ngb-toast>
</ng-template>
</ng-container>

View file

@ -0,0 +1,7 @@
:host {
position: fixed;
bottom: 0;
right: 0;
margin: 0.5em;
z-index: 1200;
}

View file

@ -0,0 +1,26 @@
// Angular modules
import { NgFor } from '@angular/common';
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
// External modules
import { NgbToast } from '@ng-bootstrap/ng-bootstrap';
import { NgbToastHeader } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
// Internal modules
import { ToastManager } from './toast.manager';
@Component({
selector : 'app-toast',
templateUrl : './toast.component.html',
styleUrls : ['./toast.component.scss'],
standalone : true,
imports : [NgFor, NgIf, NgbToast, NgbToastHeader, TranslateModule]
})
export class ToastComponent
{
constructor(public toastManager : ToastManager) {}
}

View file

@ -0,0 +1,52 @@
// Angular modules
import { Injectable } from '@angular/core';
export type ToastType = 'success' | 'info' | 'warning' | 'danger';
export class Toast
{
public id !: number
readonly headerKey ?: string;
public withHeader : boolean;
public body : string;
public type : ToastType;
public autoHide : boolean;
public delay : number;
constructor(body : string, type ?: ToastType, autoHide : boolean = false)
{
this.withHeader = true;
this.body = body;
this.type = type ?? 'danger';
this.autoHide = autoHide;
this.delay = 10000; // 10 sec
this.headerKey = this.type.toUpperCase();
}
}
@Injectable({ providedIn : 'root' })
export class ToastManager
{
public toasts : Toast[] = [];
private counter : number = 0;
constructor() {}
public show(toast : Toast) : void
{
toast.id = this.counter++;
this.toasts.push(toast);
}
public quickShow(body : string, type ?: ToastType, autoHide : boolean = false) : void
{
const toast = new Toast(body, type, autoHide);
this.show(toast);
}
public remove(id : number) : void
{
this.toasts = this.toasts.filter(t => t.id !== id);
}
}

View file

@ -0,0 +1,17 @@
<!-- NOTE Confirm form -->
<form (ngSubmit)="onClickSubmit()">
<div class="text-center py-3">
<p>{{ 'ALERT_DELETE_ENTRY' | translate }}</p>
</div>
<!-- NOTE Submit -->
<div class="text-center">
<button type="button" class="btn btn-light" (click)="onClickClose()">
{{ 'CLOSE' | translate }}
</button>
<button type="submit" class="btn btn-danger ms-3">
{{ 'DELETE' | translate }}
</button>
</div>
</form>

View file

@ -0,0 +1,45 @@
// Angular modules
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import { Input } from '@angular/core';
import { Output } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms';
// External modules
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector : 'app-form-confirm',
templateUrl : './form-confirm.component.html',
styleUrls : ['./form-confirm.component.scss'],
standalone : true,
imports : [FormsModule, TranslateModule]
})
export class FormConfirmComponent implements OnInit
{
@Input() data : any;
@Output() submitData : EventEmitter<boolean> = new EventEmitter();
@Output() submitClose : EventEmitter<null> = new EventEmitter();
constructor() { }
public ngOnInit() : void
{
}
// -------------------------------------------------------------------------------
// NOTE Action -------------------------------------------------------------------
// -------------------------------------------------------------------------------
public async onClickSubmit() : Promise<void>
{
this.submitData.emit(true);
}
public onClickClose() : void
{
this.submitClose.emit();
}
}

View file

@ -0,0 +1,35 @@
<!-- NOTE Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" [routerLink]="['/dashboard']">
<img src="./assets/img/project/logo.svg" [alt]="appName + ' Logo'" class="img-fluid me-2"/>
{{ appName }}
</a>
<button class="navbar-toggler" type="button" (click)="isMenuCollapsed = !isMenuCollapsed" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" [ngbCollapse]="isMenuCollapsed" id="navbarSupportedContent">
<ul class="navbar-nav mb-2 mb-lg-0 w-100">
<li class="nav-item">
<a class="nav-link" [routerLink]="['/home']" [routerLinkActive]="['active']">{{ 'HOME' | translate }}</a>
</li>
<li class="nav-item dropdown" ngbDropdown>
<a class="nav-link dropdown-toggle" ngbDropdownToggle id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'USER' | translate }}
</a>
<ul class="dropdown-menu dropdown-menu-end" ngbDropdownMenu aria-labelledby="navbarDropdown">
<!-- <li><a class="dropdown-item" [routerLink]="['/user']" [routerLinkActive]="['active']">{{ 'MY_ACCOUNT' | translate }}</a></li> -->
<li><a class="dropdown-item cursor-pointer" (click)="onClickLogout()">{{ 'LOGOUT' | translate }}</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- NOTE Content -->
<ng-content></ng-content>

View file

@ -0,0 +1,20 @@
.navbar-brand {
img {
width: 38px;
display: inline-block;
vertical-align: middle;
}
.title-wrapper {
vertical-align: middle;
display: inline-block;
border-left: 1px solid #adadad;
padding-left: 15px;
margin-left: 15px;
span {
display: block;
&.subtitle {
font-size: 71%;
}
}
}
}

View file

@ -0,0 +1,73 @@
// Angular modules
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { RouterLink } from '@angular/router';
import { RouterLinkActive } from '@angular/router';
// External modules
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap';
import { NgbDropdownMenu } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
// Internal modules
import { environment } from '@env/environment';
@Component({
selector : 'app-layout-header',
templateUrl : './layout-header.component.html',
styleUrls : ['./layout-header.component.scss'],
standalone : true,
imports : [RouterLink, NgbCollapse, RouterLinkActive, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, TranslateModule]
})
export class LayoutHeaderComponent implements OnInit
{
public appName : string = environment.appName;
public isMenuCollapsed : boolean = true;
constructor
(
private router : Router,
)
{
}
public ngOnInit() : void
{
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
public async onClickLogout() : Promise<void>
{
// NOTE Redirect to login
this.router.navigate(['/auth/login']);
}
// -------------------------------------------------------------------------------
// NOTE Computed props -----------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Subscriptions ------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,5 @@
<app-layout-header>
<ng-content></ng-content>
</app-layout-header>

View file

@ -0,0 +1,51 @@
// Angular modules
import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
// Components
import { LayoutHeaderComponent } from '../layout-header/layout-header.component';
@Component({
selector : 'app-page-layout',
templateUrl : './page-layout.component.html',
styleUrls : ['./page-layout.component.scss'],
standalone : true,
imports : [LayoutHeaderComponent]
})
export class PageLayoutComponent implements OnInit
{
constructor()
{
}
public ngOnInit() : void
{
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Actions ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Computed props -----------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Requests -----------------------------------------------------------------
// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------
// NOTE Subscriptions ------------------------------------------------------------
// -------------------------------------------------------------------------------
}

View file

@ -0,0 +1,12 @@
<div class="modal-header {{ modalData.headerClasses }}">
<!-- NOTE Dynamic title -->
<h6 class="modal-title">{{ modalData.title }}</h6>
<button type="button" class="btn-close" *ngIf="modalData.closable" aria-label="Close" (click)="activeModal.dismiss()">
</button>
</div>
<div class="modal-body auto">
<!-- NOTE Component -->
<ng-template modal-wrapper-host></ng-template>
</div>

View file

@ -0,0 +1,9 @@
.modal-header {
background: var(--bs-info);
&, button {
color: white;
}
.modal-title {
text-transform: uppercase;
}
}

View file

@ -0,0 +1,103 @@
// Angular modules
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { Input } from '@angular/core';
import { OnInit } from '@angular/core';
import { ViewChild } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { Type } from '@angular/core';
// External modules
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
// Directives
import { ModalWrapperDirective as ModalWrapperDirective_1 } from '../../../directives/modal-wrapper.directive';
import { ModalWrapperDirective } from '@directives/modal-wrapper.directive';
// Dynamic component loader - Angular
// https://angular.io/guide/dynamic-component-loader
// https://stackblitz.com/angular/bydvgpxaabj?file=src%2Fapp%2Fad-banner.component.ts
export class ModalForm
{
constructor(public component : Type<any>, public data : any) {}
}
export interface FormComponent
{
data : any;
submitData : EventEmitter<any>;
submitClose : EventEmitter<null>;
}
@Component({
selector : 'app-modal-wrapper',
templateUrl : 'modal-wrapper.component.html',
styleUrls : ['modal-wrapper.component.scss'],
standalone : true,
imports : [NgIf, ModalWrapperDirective_1]
})
export class ModalWrapperComponent implements OnInit
{
@Input() component : any;
@Input() componentData : any;
@Input() modalData : {
title : string,
headerClasses : string,
closable : boolean,
} = {
title : '',
headerClasses : '',
closable : true,
};
@ViewChild(ModalWrapperDirective, { static : true }) modalWrapperHost !: ModalWrapperDirective;
constructor(public activeModal : NgbActiveModal)
{
}
public ngOnInit() : void
{
this.loadComponent();
}
// -------------------------------------------------------------------------------
// NOTE Init ---------------------------------------------------------------------
// -------------------------------------------------------------------------------
private loadComponent() : void
{
const modalForm = new ModalForm(this.component, this.componentData);
const viewContainerRef = this.modalWrapperHost.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(modalForm.component);
(<FormComponent>componentRef.instance).data = modalForm.data;
(<FormComponent>componentRef.instance).submitClose.subscribe(() =>
{
this.submitClose();
});
(<FormComponent>componentRef.instance).submitData.subscribe(event =>
{
this.submitData(event);
});
}
// -------------------------------------------------------------------------------
// NOTE Helpers ------------------------------------------------------------------
// -------------------------------------------------------------------------------
private submitData($event : any) : void
{
this.activeModal.close($event);
}
private submitClose() : void
{
this.activeModal.close();
}
}

View file

@ -0,0 +1,12 @@
// Angular modules
import { Directive } from '@angular/core';
import { ViewContainerRef } from '@angular/core';
@Directive({
selector : '[modal-wrapper-host]',
standalone : true,
})
export class ModalWrapperDirective
{
constructor(public viewContainerRef : ViewContainerRef) { }
}

View file

@ -0,0 +1,6 @@
export enum Endpoint
{
AUTHENTICATE = 'authenticate',
FORGOT_PASSWORD = 'forgot-password',
VALIDATE_ACCOUNT = 'validate-account',
}

View file

@ -0,0 +1,5 @@
export enum EnvName
{
LOCAL = 'local',
PROD = 'production',
}

View file

@ -0,0 +1,4 @@
export enum StorageKey
{
TOKEN = 'Token',
}

View file

@ -0,0 +1,117 @@
// Angular modules
import { Injectable } from '@angular/core';
// Internal modules
import { environment } from '@env/environment';
// Enums
import { StorageKey } from '@enums/storage-key.enum';
// Models
// import { AuthResponse } from '@models/auth-response.model';
@Injectable()
export class StorageHelper
{
private static storagePrefix : string = environment.appName + '_' + environment.version + '_';
// ----------------------------------------------------------------------------------------------
// SECTION Methods ------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Token
// public static setToken(user : AuthResponse) : void
// {
// StorageHelper.setItem(StorageKey.TOKEN, user);
// }
// public static removeToken() : void
// {
// StorageHelper.removeItem(StorageKey.TOKEN);
// }
// public static getToken() : AuthResponse | null
// {
// // Prevent SSR error
// if (typeof localStorage === 'undefined')
// return undefined;
// const data = StorageHelper.getItem(StorageKey.TOKEN);
// return data ? new AuthResponse(data) : null;
// }
// !SECTION Methods
// ----------------------------------------------------------------------------------------------
// SECTION LocalStorage -------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
public static setItem(key : string, value : any, prefix : boolean = true) : void
{
const itemKey = this.prefixer(key, prefix);
localStorage.setItem(itemKey, JSON.stringify(value));
}
public static getItem(key : string, prefix : boolean = true) : any
{
const itemKey = this.prefixer(key, prefix);
const res = localStorage.getItem(itemKey);
if (res !== 'undefined')
return JSON.parse(res as any);
console.error('StorageHelper : getItem -> undefined key');
return null;
}
public static removeItem(key : string, prefix : boolean = true) : void
{
const itemKey = this.prefixer(key, prefix);
localStorage.removeItem(itemKey);
}
public static getKeys(all : boolean = false) : string[]
{
const keys : string[] = [];
// NOTE Keys
for (const key in localStorage)
keys.push(key);
if (all)
return keys;
// NOTE Prefixed keys
return keys.filter((item) => item.startsWith(this.storagePrefix));
}
public static clearItems(all : boolean = false) : void
{
// NOTE Keys
if (all)
{
localStorage.clear();
return;
}
// NOTE Prefixed keys
const prefixedKeys = this.getKeys();
for (const prefixedKey of prefixedKeys)
this.removeItem(prefixedKey, false);
}
public static clearItemsWithoutCurrentPrefix() : void
{
const allKeys = this.getKeys(true);
for (const key of allKeys)
if (!key.startsWith(this.storagePrefix))
this.removeItem(key, false);
}
// !SECTION LocalStorage
// NOTE Private
private static prefixer(key : string, autoPrefix : boolean) : string
{
let itemKey = key;
if (autoPrefix)
itemKey = this.storagePrefix + key;
return itemKey;
}
}

View file

@ -0,0 +1,30 @@
// Angular modules
import { Injectable } from '@angular/core';
@Injectable()
export class StringHelper
{
/** JavaScript equivalent to printf/String.Format
* https://stackoverflow.com/a/31007976/7462178
*/
public static interpolate(theString : string, argumentArray : string[]) : string
{
let regex = /%s/;
let _r = function(p : string, c : string) { return p.replace(regex, c); };
return argumentArray.reduce(_r, theString);
}
public static buildURIParams(parameters : object) : string
{
const parts = new URLSearchParams()
for (const [key, value] of Object.entries(parameters))
{
if (key === undefined || value === undefined)
continue;
parts.append(key, value)
}
return '?' + parts.toString()
}
}

View file

@ -0,0 +1,177 @@
// Angular modules
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
// External modules
import { ArrayTyper } from '@caliatys/array-typer';
import { TranslateService } from '@ngx-translate/core';
import axios from 'axios';
import { AxiosResponse } from 'axios';
import { AxiosError } from 'axios';
import { AxiosInstance } from 'axios';
import { CreateAxiosDefaults } from 'axios';
// Internal modules
import { ToastManager } from '@blocks/toast/toast.manager';
import { environment } from '@env/environment';
// Helpers
import { StorageHelper } from '@helpers/storage.helper';
// Enums
import { Endpoint } from '@enums/endpoint.enum';
// Models
// Services
import { StoreService } from './store.service';
@Injectable()
export class AppService
{
// NOTE Default configuration
private default : CreateAxiosDefaults = {
withCredentials : true,
timeout : 990000,
headers : {
'Content-Type' : 'application/json',
'Accept' : 'application/json',
},
};
// NOTE Instances
private api : AxiosInstance = axios.create({
baseURL : environment.apiBaseUrl,
...this.default,
});
// NOTE Controller
private controller : AbortController = new AbortController();
constructor
(
private storeService : StoreService,
private toastManager : ToastManager,
private router : Router,
private translateService : TranslateService,
)
{
this.initRequestInterceptor(this.api);
this.initResponseInterceptor(this.api);
this.initAuthHeader();
}
// ----------------------------------------------------------------------------------------------
// SECTION Methods ------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
public async authenticate(email : string, password : string) : Promise<boolean>
{
return Promise.resolve(true);
// StorageHelper.removeToken();
// const url = Endpoint.AUTHENTICATE;
// const { data } = await this.api.post(url, { email, password });
// if (!data)
// return false;
// const authResponse = new AuthResponse(data);
// StorageHelper.setToken(authResponse);
// this.initAuthHeader();
// return true;
}
public async forgotPassword(email : string) : Promise<boolean>
{
return Promise.resolve(true);
// const url = Endpoint.FORGOT_PASSWORD;
// const { data } = await this.api.post(url, { email });
// return !!data;
}
public async validateAccount(token : string, password : string) : Promise<boolean>
{
return Promise.resolve(true);
// const url = Endpoint.VALIDATE_ACCOUNT;
// const { data } = await this.api.post(url, { token, password });
// return !!data;
}
// public async getLastLines(siteId : string) : Promise<Line[]>
// {
// const url = StringHelper.interpolate(Endpoint.GET_LAST_LINES, [ siteId ]);
// const { data } = await this.api.get(url);
// if (!data)
// return [];
// return ArrayTyper.asArray(Line, data);
// }
// !SECTION Methods
// ----------------------------------------------------------------------------------------------
// SECTION Helpers ------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
private initAuthHeader() : void
{
// const token = StorageHelper.getToken();
// if (!token)
// return;
// this.api.defaults.headers.common['Authorization'] = `Bearer ${token.jwtToken}`;
// this.api.defaults.headers.common['Token'] = token.jwtToken;
}
public initRequestInterceptor(instance : AxiosInstance) : void
{
instance.interceptors.request.use((config) =>
{
console.log('interceptors.request.config', config);
this.storeService.isLoading.set(true);
return config;
},
(error) =>
{
console.log('interceptors.request.error', error);
this.storeService.isLoading.set(false);
this.toastManager.quickShow(error);
return Promise.reject(error);
});
}
public initResponseInterceptor(instance : AxiosInstance) : void
{
instance.interceptors.response.use((response) =>
{
console.log('interceptors.response.response', response);
this.storeService.isLoading.set(false);
return response;
},
async (error : AxiosError) =>
{
console.log('interceptors.response.error', error);
this.storeService.isLoading.set(false);
// NOTE Prevent request canceled error
if (error.code === 'ERR_CANCELED')
return Promise.resolve(error);
this.toastManager.quickShow(error.message);
return Promise.reject(error);
});
}
// !SECTION Helpers
}

View file

@ -0,0 +1,38 @@
// Angular modules
import { isPlatformServer } from '@angular/common';
import { Inject } from '@angular/core';
import { signal } from '@angular/core';
import { Injectable } from '@angular/core';
import { PLATFORM_ID } from '@angular/core';
// External modules
import { TranslateService } from '@ngx-translate/core';
// Internal modules
import { environment } from '@env/environment';
@Injectable()
export class StoreService
{
public isServer = signal(isPlatformServer(this.platformId));
public isLoading = signal(true);
public pageTitle = signal(environment.appName);
constructor
(
@Inject(PLATFORM_ID) private platformId : Object,
private translateService : TranslateService
)
{
}
// -------------------------------------------------------------------------------
// NOTE Page title ---------------------------------------------------------------
// -------------------------------------------------------------------------------
public setPageTitle(title : string, translate : boolean = true) : void
{
const pageTitle = translate ? this.translateService.instant(title) : title;
this.pageTitle.set(pageTitle);
}
}

View file

View file

@ -0,0 +1,24 @@
{
"NOT_FOUND" : "404 - Page not found",
"GO_TO_HOMEPAGE" : "Go to homepage",
"WELCOME_TO" : "Welcome to",
"PROJECT_DESC" : "A Centurio boilerplate solution",
"SIGN_IN" : "Sign in",
"LOGOUT" : "Logout",
"HOME" : "Home",
"VERSION" : "Version",
"USER" : "User",
"EMAIL" : "E-mail",
"FORGOT_YOUR_PWD" : "Forgot your password?",
"BACK_TO_LOGIN" : "Back to login",
"VALIDATE_ACCOUNT" : "Validate the account",
"VALIDATE" : "Validate",
"PASSWORD" : "Password",
"LOADING" : "Loading",
"CLOSE" : "Close",
"DELETE" : "Delete",
"ALERT_DELETE_ENTRY" : "Are you sure you want to delete this entry?",
"RESET_PASSWORD" : "Reset password",
"FIELD_REQUIRED" : "This field is required",
"FIELD_EMAIL" : "This field must be an email"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="mstile-150x150.png"/>
<TileColor>#2d89ef</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,21 @@
{
"name": "EasyAngular",
"short_name": "EasyAngular",
"theme_color": "#2b46c7",
"background_color": "#2b46c7",
"display": "standalone",
"scope": "/",
"start_url": "",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,44 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3298 6995 c-2 -1 -48 -6 -102 -9 -55 -4 -102 -8 -105 -10 -3 -2 -35
-7 -70 -10 -35 -4 -85 -11 -110 -16 -25 -5 -57 -12 -71 -14 -83 -14 -199 -42
-304 -73 -125 -36 -364 -119 -381 -133 -5 -4 -21 -11 -35 -15 -42 -13 -228
-105 -340 -169 -188 -106 -468 -302 -453 -317 7 -7 538 -291 873 -467 58 -30
128 -67 155 -82 28 -15 88 -48 135 -72 185 -99 249 -132 340 -181 52 -27 118
-62 145 -77 28 -15 100 -53 160 -85 61 -32 133 -70 160 -85 28 -15 89 -48 138
-74 222 -118 417 -221 502 -266 39 -21 216 -115 395 -210 179 -95 358 -190
398 -211 39 -22 107 -58 150 -80 42 -23 109 -58 147 -79 39 -20 108 -58 155
-82 47 -25 119 -63 160 -85 41 -22 120 -64 175 -93 55 -30 145 -77 200 -107
55 -29 190 -101 301 -159 l201 -106 1 -1156 c0 -637 1 -1161 1 -1165 2 -22
225 294 308 438 57 98 153 292 203 410 71 166 165 468 195 624 56 291 69 431
69 711 0 186 -6 326 -19 420 -3 19 -7 53 -10 75 -50 407 -218 897 -435 1265
-53 90 -191 301 -214 327 -18 21 -31 37 -55 71 -87 124 -326 373 -501 523
-121 104 -424 319 -449 319 -5 0 -11 4 -13 8 -8 20 -306 171 -473 239 -196 81
-434 153 -630 192 -126 25 -213 38 -375 57 -56 6 -516 15 -522 9z"/>
<path d="M1260 6188 c-96 -77 -312 -289 -391 -383 -19 -22 -42 -49 -53 -60
-37 -40 -164 -212 -233 -315 -180 -270 -311 -541 -411 -850 -38 -118 -79 -264
-88 -320 -3 -14 -11 -56 -19 -95 -15 -78 -23 -127 -39 -247 -27 -197 -28 -722
-1 -828 2 -8 6 -40 10 -71 3 -31 8 -67 10 -80 3 -13 7 -39 10 -58 2 -18 7 -41
9 -50 3 -9 8 -31 11 -49 13 -75 77 -306 119 -430 223 -647 652 -1236 1196
-1641 435 -324 918 -542 1415 -640 97 -19 127 -24 210 -36 22 -3 54 -8 70 -10
120 -19 544 -26 705 -11 146 13 164 15 195 20 17 3 48 8 70 12 22 3 74 12 115
20 41 9 89 18 105 21 53 9 327 91 425 127 169 61 484 208 518 240 11 11 17
279 13 556 -1 32 -6 37 -59 65 -31 16 -91 48 -132 71 -41 22 -131 70 -200 106
-69 36 -161 85 -205 109 -44 23 -129 69 -190 101 -60 32 -129 69 -152 80 l-43
22 -1 60 c0 34 0 269 -1 523 0 328 -4 465 -12 471 -6 5 -73 41 -149 81 -76 39
-157 82 -180 95 -51 29 -210 113 -462 246 -55 29 -122 65 -150 80 -27 15 -99
53 -160 85 -60 32 -163 86 -227 121 -64 35 -119 64 -121 64 -3 0 -40 19 -83
43 -73 40 -130 71 -379 202 -49 26 -117 62 -150 80 -33 18 -105 56 -160 85
-129 68 -219 116 -325 173 -191 102 -298 159 -347 184 l-53 26 -2 1013 -3
1012 -25 -20z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,48 @@
// ----------------------------------------------------------------------------------------------
// NOTE Browser style : Reset --------------------------------------------------------- Overwrite
// ----------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Project ---------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Link
// NOTE Scrollbar
// NOTE Sortable table --------------------------------------------------------------------------
// NOTE Icons -----------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Bootstrap --------------------------------------------------------------------- Overwrite
// ----------------------------------------------------------------------------------------------
// NOTE Pagination ------------------------------------------------------------------------------
// NOTE Button ----------------------------------------------------------------------------------
// NOTE Modal -----------------------------------------------------------------------------------
// NOTE Toast -----------------------------------------------------------------------------------
.toast-header {
background-color: transparent;
}
.toast-body {
background-color: rgba(255, 255, 255, 0.85);
}
// NOTE Nav -------------------------------------------------------------------------------------
// NOTE Table -----------------------------------------------------------------------------------
// NOTE Select ----------------------------------------------------------------------------------
// NOTE Form ------------------------------------------------------------------------------------
// Checkbox
// Radio
// NOTE Sortable table --------------------------------------------------------------------------

View file

@ -0,0 +1,80 @@
// ----------------------------------------------------------------------------------------------
// NOTE Variables -------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
@import 'variables.scss';
// ----------------------------------------------------------------------------------------------
// NOTE Bootstrap -------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Complete bootstrap
@import '../../../node_modules/bootstrap/scss/bootstrap.scss';
// NOTE Bootstrap configuration
// @import '../../../node_modules/bootstrap/scss/functions';
// @import '../../../node_modules/bootstrap/scss/variables';
// @import '../../../node_modules/bootstrap/scss/mixins';
// // NOTE Merge maps
// $theme-colors: map-merge($theme-colors, $custom-colors);
// @import '../../../node_modules/bootstrap/scss/utilities';
// // NOTE Bootstrap layout & components
// @import '../../../node_modules/bootstrap/scss/root';
// @import '../../../node_modules/bootstrap/scss/reboot';
// @import '../../../node_modules/bootstrap/scss/type';
// // @import '../../../node_modules/bootstrap/scss/images';
// @import '../../../node_modules/bootstrap/scss/containers';
// @import '../../../node_modules/bootstrap/scss/grid';
// @import '../../../node_modules/bootstrap/scss/tables';
// @import '../../../node_modules/bootstrap/scss/forms';
// @import '../../../node_modules/bootstrap/scss/buttons';
// @import '../../../node_modules/bootstrap/scss/transitions';
// @import '../../../node_modules/bootstrap/scss/dropdown';
// // @import '../../../node_modules/bootstrap/scss/button-group';
// @import '../../../node_modules/bootstrap/scss/nav';
// @import '../../../node_modules/bootstrap/scss/navbar';
// @import '../../../node_modules/bootstrap/scss/card';
// // @import '../../../node_modules/bootstrap/scss/accordion';
// // @import '../../../node_modules/bootstrap/scss/breadcrumb';
// // @import '../../../node_modules/bootstrap/scss/pagination';
// @import '../../../node_modules/bootstrap/scss/badge';
// // @import '../../../node_modules/bootstrap/scss/alert';
// // @import '../../../node_modules/bootstrap/scss/progress';
// // @import '../../../node_modules/bootstrap/scss/list-group';
// // @import '../../../node_modules/bootstrap/scss/close';
// @import '../../../node_modules/bootstrap/scss/toasts';
// @import '../../../node_modules/bootstrap/scss/modal';
// @import '../../../node_modules/bootstrap/scss/tooltip';
// @import '../../../node_modules/bootstrap/scss/popover';
// // @import '../../../node_modules/bootstrap/scss/carousel';
// @import '../../../node_modules/bootstrap/scss/spinners';
// // @import '../../../node_modules/bootstrap/scss/offcanvas';
// // NOTE Boostrap helpers
// @import '../../../node_modules/bootstrap/scss/helpers';
// // NOTE Boostrap utilities
// @import '../../../node_modules/bootstrap/scss/utilities/api';
// ----------------------------------------------------------------------------------------------
// NOTE External components ---------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// $roboto-font-path: "../../../node_modules/roboto-fontface/fonts" !default;
// @import "../../../node_modules/roboto-fontface/css/roboto/sass/roboto-fontface";
// @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;400;700&display=swap');
// ----------------------------------------------------------------------------------------------
// NOTE Project style ---------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
@import 'project.scss';
// ----------------------------------------------------------------------------------------------
// NOTE Overwrite styles ------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// @import 'modules/_bootstrap.scss';

View file

@ -0,0 +1,66 @@
// ----------------------------------------------------------------------------------------------
// SECTION Variables ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Project ---------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Colors ----------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Bootstrap -------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
// NOTE Utilities -------------------------------------------------------------------------------
$utilities: (
"cursor": (
property: cursor,
class: cursor,
responsive: false,
values: pointer,
)
);
// NOTE Color system ----------------------------------------------------------------------------
// $primary: #375F9B;
$secondary: #8492A6;
$text-muted: #ACA7AA;
$info: #375F9B; // #3C78DA;
$danger: #D0021B;
$success: #12a366;
$min-contrast-ratio: 2.5;
// NOTE Body ------------------------------------------------------------------------------------
// NOTE Grid ------------------------------------------------------------------------------------
// NOTE Borders ---------------------------------------------------------------------------------
// NOTE Typography -----------------------------------------------------------------------------
// NOTE Links -----------------------------------------------------------------------------------
// NOTE Badges ----------------------------------------------------------------------------------
// NOTE Buttons ---------------------------------------------------------------------------------
$btn-close-color: white;
// NOTE Navbar ----------------------------------------------------------------------------------
// NOTE Navs ------------------------------------------------------------------------------------
// NOTE Modal -----------------------------------------------------------------------------------
// NOTE Dropdowns -------------------------------------------------------------------------------
// NOTE Cards -----------------------------------------------------------------------------------
// NOTE List group ------------------------------------------------------------------------------
// NOTE Pagination ------------------------------------------------------------------------------
// NOTE Popovers --------------------------------------------------------------------------------
// NOTE Typography ------------------------------------------------------------------------------
// NOTE Forms -----------------------------------------------------------------------------------
// NOTE Tables ----------------------------------------------------------------------------------
// !SECTION Variables

View file

@ -0,0 +1,21 @@
// Enums
import { EnvName } from '@enums/environment.enum';
// Packages
import packageInfo from '../../package.json';
const scheme = 'http://';
const host = 'localhost';
const port = ':5000';
const path = '/api/';
const baseUrl = scheme + host + port + path;
export const environment = {
production : true,
version : packageInfo.version,
appName : 'EasyAngular',
envName : EnvName.PROD,
defaultLanguage : 'en',
apiBaseUrl : baseUrl,
};

View file

@ -0,0 +1,34 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
// Enums
import { EnvName } from '@enums/environment.enum';
// Packages
import packageInfo from '../../package.json';
const scheme = 'http://';
const host = 'localhost';
const port = ':5000';
const path = '/api/';
const baseUrl = scheme + host + port + path;
export const environment = {
production : false,
version : packageInfo.version,
appName : 'EasyAngular',
envName : EnvName.LOCAL,
defaultLanguage : 'en',
apiBaseUrl : baseUrl,
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

25
angular18/src/index.html Normal file
View file

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>EasyAngular</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="./assets/img/favicon/favicon.ico">
<!-- NOTE RealFavIconGenerator -->
<link rel="apple-touch-icon" sizes="180x180" href="assets/img/favicon/apple-touch-icon.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/favicon/favicon-16x16.png"/>
<link rel="manifest" href="assets/img/favicon/manifest.json"/>
<link rel="mask-icon" href="assets/img/favicon/safari-pinned-tab.svg" color="#5bbad5"/>
<meta name="msapplication-TileColor" content="#da532c"/>
<meta name="theme-color" content="#ffffff"/>
</head>
<body>
<app-root></app-root>
</body>
</html>

24
angular18/src/main.ts Normal file
View file

@ -0,0 +1,24 @@
/// <reference types="@angular/localize" />
// Angular modules
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
// External modules
import { appConfig } from './app/app.config';
// Internal modules
import { environment } from './environments/environment';
// Components
import { AppComponent } from './app/app.component';
if (environment.production) {
enableProdMode();
}
bootstrapApplication(
AppComponent,
appConfig
)
.catch(err => console.error(err));

View file

@ -0,0 +1,54 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
import '@angular/localize/init';

14
angular18/src/test.ts Normal file
View file

@ -0,0 +1,14 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);