├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ ├── app.routes.ts │ ├── auth │ │ ├── auth-api.service.ts │ │ ├── auth-routing.module.ts │ │ ├── auth-state.model.ts │ │ ├── auth.actions.ts │ │ ├── auth.component.html │ │ ├── auth.component.scss │ │ ├── auth.component.ts │ │ ├── auth.guard.ts │ │ ├── auth.interceptor.ts │ │ ├── auth.model.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── auth.state.ts │ │ ├── sign-in │ │ │ ├── sign-in.component.html │ │ │ ├── sign-in.component.scss │ │ │ ├── sign-in.component.ts │ │ │ └── sign-in.model.ts │ │ └── sign-up │ │ │ ├── sign-up.component.html │ │ │ ├── sign-up.component.scss │ │ │ ├── sign-up.component.ts │ │ │ └── sign-up.model.ts │ ├── helpers │ │ ├── browser-storage.service.ts │ │ └── password-validators.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ └── home.module.ts │ ├── models │ │ └── routes.model.ts │ ├── services │ │ └── error-handler.service.ts │ └── shared │ │ ├── footer │ │ ├── footer.component.html │ │ ├── footer.component.scss │ │ └── footer.component.ts │ │ ├── header │ │ ├── header.component.html │ │ ├── header.component.scss │ │ └── header.component.ts │ │ ├── index.ts │ │ └── preloader │ │ ├── preloader.component.html │ │ ├── preloader.component.scss │ │ └── preloader.component.ts ├── assets │ ├── .gitkeep │ ├── i18n │ │ └── en.json │ └── styles │ │ ├── media.scss │ │ ├── normalize.scss │ │ └── variables.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Valletta Software Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate: Angular 17 Web client application 2 | 3 | A base functional Angular project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.7. 4 | 5 | _Change `PromoBoilerplateAngular` to your project name._ 6 | 7 | ## Development server 8 | 9 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 10 | 11 | ## Features Summary: 12 | 13 | - Base ctructure for starting project on Angular 17 14 | - Configured environment 15 | - Authorization module with base auth forms, service, guards and state 16 | - State management by [NGXS](https://www.ngxs.io/) 17 | - Internalization by [ngx-translate](https://github.com/ngx-translate/core) 18 | - Testing by Karma 19 | 20 | ## Angular 17 21 | 22 | The new Angular 17 version was released on November 8, 2023. 23 | And the [Valletta Software](https://www.vallettasoftware.com/) team is already ready to develop and implement solutions for your business based on this modern stack. 24 | 25 | ## Base ctructure 26 | 27 | We prepared base structure for new project. 28 | It has components and services what used in every real project. 29 | This boilerplate will help you get start new project faster. 30 | 31 | ## Authorization module 32 | 33 | Auth module has base logic and structure 34 | 35 | - AuthModule defines all auth entities 36 | - AuthService provides data from store, methods for components and dispathes auth actions 37 | - AuthApiServices makes requests to Auth endpoint 38 | - AuthState store data about user's authorization 39 | - AuthGuard provides CanActivate functions for security implementation 40 | - SignIn and SignUp components have forms for login and registration 41 | 42 | ## State management 43 | 44 | Included state managment by NGXS package. NGXS is modeled after the CQRS pattern popularly implemented in libraries like Redux and NgRx but reduces boilerplate by using modern TypeScript features such as classes and decorators. 45 | Also we configured following really useful plugins: 46 | 47 | - [Logger](https://www.ngxs.io/plugins/logger) outputs state and it's changes 48 | - [Storage](https://www.ngxs.io/plugins/storage) saves selected parts of store between app reloading. It uses localStorage and restores previous state 49 | - [Router](https://www.ngxs.io/plugins/router) allows to navigate by actions 50 | 51 | AuthState can be used as example (State, Actions, Selectors) for your new states. 52 | 53 | ## Internalization 54 | 55 | Native internalization in Angular very cumbersome and difficult. 56 | Internalization with [ngx-translate](https://github.com/ngx-translate/core) is so easy. 57 | Just create JSON dictionary `/assets/i18n/[lang].json` for each language and output it with translation pipe: `{{ 'MULTI-LEVEL.KEY' | translate }}`. 58 | 59 | ## Testing 60 | 61 | Karma is available in the boilerplate 62 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "promo-boilerplate-angular": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist/promo-boilerplate-angular", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": [ 24 | "zone.js" 25 | ], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "src/styles.scss" 34 | ], 35 | "scripts": [] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "budgets": [ 46 | { 47 | "type": "initial", 48 | "maximumWarning": "500kb", 49 | "maximumError": "1mb" 50 | }, 51 | { 52 | "type": "anyComponentStyle", 53 | "maximumWarning": "2kb", 54 | "maximumError": "4kb" 55 | } 56 | ], 57 | "outputHashing": "all" 58 | }, 59 | "development": { 60 | "optimization": false, 61 | "extractLicenses": false, 62 | "sourceMap": true 63 | } 64 | }, 65 | "defaultConfiguration": "production" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "configurations": { 70 | "production": { 71 | "buildTarget": "promo-boilerplate-angular:build:production" 72 | }, 73 | "development": { 74 | "buildTarget": "promo-boilerplate-angular:build:development" 75 | } 76 | }, 77 | "defaultConfiguration": "development" 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "buildTarget": "promo-boilerplate-angular:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "polyfills": [ 89 | "zone.js", 90 | "zone.js/testing" 91 | ], 92 | "tsConfig": "tsconfig.spec.json", 93 | "inlineStyleLanguage": "scss", 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ], 98 | "styles": [ 99 | "src/styles.scss" 100 | ], 101 | "scripts": [] 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promo-boilerplate-angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^17.0.0", 14 | "@angular/common": "^17.0.0", 15 | "@angular/compiler": "^17.0.0", 16 | "@angular/core": "^17.0.0", 17 | "@angular/forms": "^17.0.0", 18 | "@angular/platform-browser": "^17.0.0", 19 | "@angular/platform-browser-dynamic": "^17.0.0", 20 | "@angular/router": "^17.0.0", 21 | "@ngx-translate/core": "^15.0.0", 22 | "@ngx-translate/http-loader": "^8.0.0", 23 | "@ngxs/logger-plugin": "^3.8.2", 24 | "@ngxs/router-plugin": "^3.8.2", 25 | "@ngxs/storage-plugin": "^3.8.2", 26 | "@ngxs/store": "^3.8.2", 27 | "rxjs": "~7.8.0", 28 | "tslib": "^2.3.0", 29 | "zone.js": "~0.14.2" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^17.0.7", 33 | "@angular/cli": "^17.0.7", 34 | "@angular/compiler-cli": "^17.0.0", 35 | "@types/jasmine": "~5.1.0", 36 | "jasmine-core": "~5.1.0", 37 | "karma": "~6.4.0", 38 | "karma-chrome-launcher": "~3.2.0", 39 | "karma-coverage": "~2.2.0", 40 | "karma-jasmine": "~5.1.0", 41 | "karma-jasmine-html-reporter": "~2.1.0", 42 | "typescript": "~5.2.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'promo-boilerplate-angular' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('promo-boilerplate-angular'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, promo-boilerplate-angular'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterOutlet } from '@angular/router'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { FooterComponent } from './shared/footer/footer.component'; 6 | import { HeaderComponent } from './shared/header/header.component'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | standalone: true, 11 | imports: [ 12 | CommonModule, 13 | TranslateModule, 14 | RouterOutlet, 15 | FooterComponent, 16 | HeaderComponent, 17 | ], 18 | templateUrl: './app.component.html', 19 | styleUrl: './app.component.scss', 20 | }) 21 | export class AppComponent { 22 | title = 'promo-boilerplate-angular'; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | import { 4 | HTTP_INTERCEPTORS, 5 | HttpClient, 6 | provideHttpClient, 7 | } from '@angular/common/http'; 8 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 9 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 10 | 11 | import { routes } from './app.routes'; 12 | import { NgxsModule } from '@ngxs/store'; 13 | import { environment } from '../environments/environment'; 14 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 15 | import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; 16 | import { AuthState } from './auth/auth.state'; 17 | import { AuthInterceptor } from './auth/auth.interceptor'; 18 | 19 | export const appConfig: ApplicationConfig = { 20 | providers: [ 21 | provideRouter(routes), 22 | provideHttpClient(), 23 | importProvidersFrom([ 24 | TranslateModule.forRoot({ 25 | defaultLanguage: 'en', 26 | loader: { 27 | provide: TranslateLoader, 28 | useFactory: (http: HttpClient) => 29 | new TranslateHttpLoader(http, './assets/i18n/', '.json'), 30 | deps: [HttpClient], 31 | }, 32 | }), 33 | NgxsModule.forRoot([AuthState], { 34 | developmentMode: !environment.production, 35 | }), 36 | NgxsLoggerPluginModule.forRoot({ 37 | collapsed: false, 38 | disabled: environment.production, 39 | // Use customLogger instead of console 40 | // logger: customLogger, 41 | // Do not log SomeAction 42 | // filter: action => getActionTypeFromInstance(action) !== SomeAction.type 43 | }), 44 | NgxsStoragePluginModule.forRoot({ 45 | key: 'auth', 46 | }), 47 | ]), 48 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 49 | ], 50 | }; 51 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { PATH } from './models/routes.model'; 3 | import { authorizedGuard, nonAuthorizedGuard } from './auth/auth.guard'; 4 | 5 | export const routes: Routes = [ 6 | { 7 | path: '', 8 | loadChildren: () => import('./home/home.module').then((m) => m.HomeModule), 9 | pathMatch: 'full', 10 | canActivate: [authorizedGuard], 11 | }, 12 | { 13 | path: PATH.root.auth, 14 | loadChildren: () => import('./auth/auth.module').then((m) => m.AuthModule), 15 | canActivate: [nonAuthorizedGuard], 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/app/auth/auth-api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { first } from 'rxjs/operators'; 4 | import { environment } from '../../environments/environment'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthApiService { 10 | private readonly LOGIN_API = `${environment.baseUrl}/auth/token`; 11 | private readonly REGISTER_API = `${environment.baseUrl}/auth/register`; 12 | 13 | constructor(private http: HttpClient) {} 14 | 15 | public login$(email: string, password: string) { 16 | return this.http.post(this.LOGIN_API, { email, password }); 17 | } 18 | 19 | public register$(email: string, password: string) { 20 | return this.http.post(this.REGISTER_API, { email, password }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { PATH } from '../models/routes.model'; 5 | import { AuthComponent } from './auth.component'; 6 | import { SignInComponent } from './sign-in/sign-in.component'; 7 | import { SignUpComponent } from './sign-up/sign-up.component'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: AuthComponent, 13 | children: [ 14 | { 15 | path: PATH.auth.signIn, 16 | component: SignInComponent, 17 | }, 18 | { 19 | path: PATH.auth.signUp, 20 | component: SignUpComponent, 21 | }, 22 | { 23 | path: '**', 24 | redirectTo: PATH.auth.signIn, 25 | pathMatch: 'full', 26 | }, 27 | ], 28 | }, 29 | ]; 30 | 31 | @NgModule({ 32 | imports: [RouterModule.forChild(routes)], 33 | exports: [RouterModule], 34 | }) 35 | export class AuthRoutingModule {} 36 | -------------------------------------------------------------------------------- /src/app/auth/auth-state.model.ts: -------------------------------------------------------------------------------- 1 | export interface AuthStateModel { 2 | token: string; 3 | username: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/auth/auth.actions.ts: -------------------------------------------------------------------------------- 1 | const AUTH_GROUP_NAME = '[AUTH]'; 2 | 3 | export class Register { 4 | static readonly type = `${AUTH_GROUP_NAME} Register`; 5 | constructor(public payload: { email: string; password: string }) {} 6 | } 7 | 8 | export class Login { 9 | static readonly type = `${AUTH_GROUP_NAME} Login`; 10 | constructor(public payload: { email: string; password: string }) {} 11 | } 12 | 13 | export class Logout { 14 | static readonly type = `${AUTH_GROUP_NAME} Logout`; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/auth/auth.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/auth.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/app/auth/auth.component.scss -------------------------------------------------------------------------------- /src/app/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'blr-auth', 5 | templateUrl: './auth.component.html', 6 | styleUrl: './auth.component.scss', 7 | }) 8 | export class AuthComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivatedRouteSnapshot, 3 | CanActivateFn, 4 | Router, 5 | RouterStateSnapshot, 6 | } from '@angular/router'; 7 | import { first, map } from 'rxjs/operators'; 8 | 9 | import { inject } from '@angular/core'; 10 | import { AuthService } from './auth.service'; 11 | import { BrowserStorageService } from '../helpers/browser-storage.service'; 12 | import { PATH } from '../models/routes.model'; 13 | 14 | export const nonAuthorizedGuard: CanActivateFn = ( 15 | next: ActivatedRouteSnapshot, 16 | state: RouterStateSnapshot 17 | ) => { 18 | const isResetPasswordPage = state.url.startsWith( 19 | `/${PATH.root.auth}/${PATH.auth.resetPassword}` 20 | ); 21 | if (isResetPasswordPage) { 22 | inject(BrowserStorageService).clearLocalStorage(); 23 | } 24 | 25 | return inject(AuthService).isAuthorized$.pipe( 26 | map((hasAuth) => (hasAuth ? inject(Router).createUrlTree(['/']) : true)) 27 | ); 28 | }; 29 | 30 | export const authorizedGuard: CanActivateFn = ( 31 | next: ActivatedRouteSnapshot, 32 | state: RouterStateSnapshot 33 | ) => { 34 | const router = inject(Router); 35 | return inject(AuthService).isAuthorized$.pipe( 36 | first(), 37 | map((hasAuth) => { 38 | console.debug(hasAuth); 39 | return !hasAuth 40 | ? router.createUrlTree(['/', PATH.root.auth, PATH.auth.resetPassword]) 41 | : true; 42 | }) 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/auth/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpEvent, 4 | HttpInterceptor, 5 | HttpHandler, 6 | HttpRequest, 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { Store } from '@ngxs/store'; 10 | import { AuthState } from './auth.state'; 11 | import { environment } from '../../environments/environment'; 12 | 13 | @Injectable() 14 | export class AuthInterceptor implements HttpInterceptor { 15 | constructor(private store: Store) {} 16 | 17 | intercept( 18 | req: HttpRequest, 19 | next: HttpHandler 20 | ): Observable> { 21 | if (!req.url.startsWith(environment.baseUrl)) { 22 | return next.handle(req); 23 | } 24 | const token = this.store.selectSnapshot(AuthState.token); 25 | const headers = req.headers.set('Authorization', `Bearer ${token}`); 26 | const authReq = req.clone({ headers }); 27 | return next.handle(authReq); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/auth/auth.model.ts: -------------------------------------------------------------------------------- 1 | export interface AuthModel { 2 | email: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AuthRoutingModule } from './auth-routing.module'; 5 | import { AuthComponent } from './auth.component'; 6 | import { SignInComponent } from './sign-in/sign-in.component'; 7 | import { SignUpComponent } from './sign-up/sign-up.component'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | import { ReactiveFormsModule } from '@angular/forms'; 10 | import { PreloaderComponent } from '../shared'; 11 | @NgModule({ 12 | declarations: [ 13 | AuthComponent, 14 | SignInComponent, 15 | SignUpComponent, 16 | ], 17 | imports: [ 18 | CommonModule, 19 | ReactiveFormsModule, 20 | AuthRoutingModule, 21 | TranslateModule, 22 | PreloaderComponent 23 | ], 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Select, Store } from '@ngxs/store'; 4 | import { Login, Logout, Register } from './auth.actions'; 5 | import { AuthState } from './auth.state'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AuthService { 11 | @Select(AuthState.isAuthenticated) isAuthorized$: Observable; 12 | 13 | constructor(private store: Store) {} 14 | 15 | public login(email: string, password: string): void { 16 | this.store.dispatch(new Login({ email, password })); 17 | } 18 | 19 | public logout(): void { 20 | this.store.dispatch(new Logout()); 21 | } 22 | 23 | public register(email: string, password: string): void { 24 | this.store.dispatch(new Register({ email, password })); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/auth/auth.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, Selector, State, StateContext } from '@ngxs/store'; 2 | import { AuthStateModel } from './auth-state.model'; 3 | import { Injectable } from '@angular/core'; 4 | import { Login, Logout, Register } from './auth.actions'; 5 | import { catchError } from 'rxjs/operators'; 6 | import { AuthApiService } from './auth-api.service'; 7 | import { ErrorHandlerService } from '../services/error-handler.service'; 8 | import { Navigate } from '@ngxs/router-plugin'; 9 | import { PATH } from '../models/routes.model'; 10 | 11 | const INITIAL_STATE: AuthStateModel = { 12 | token: null, 13 | username: null, 14 | }; 15 | 16 | @State({ 17 | name: 'auth', 18 | defaults: INITIAL_STATE, 19 | }) 20 | @Injectable() 21 | export class AuthState { 22 | @Selector() 23 | static token(state: AuthStateModel): string | null { 24 | return state.token; 25 | } 26 | 27 | @Selector() 28 | static isAuthenticated(state: AuthStateModel): boolean { 29 | return !!state.token; 30 | } 31 | 32 | constructor( 33 | private authApiService: AuthApiService, 34 | private errorHandler: ErrorHandlerService 35 | ) {} 36 | 37 | @Action(Register) 38 | register(ctx: StateContext, action: Register) { 39 | return this.authApiService 40 | .register$(action.payload.email, action.payload.password) 41 | .pipe(catchError(this.errorHandler.handle)) 42 | .subscribe((token) => { 43 | ctx.patchState({ 44 | token, 45 | username: action.payload.email, 46 | }); 47 | }); 48 | } 49 | 50 | @Action(Login) 51 | login(ctx: StateContext, action: Login) { 52 | return this.authApiService 53 | .login$(action.payload.email, action.payload.password) 54 | .pipe(catchError(this.errorHandler.handle)) 55 | .subscribe((token) => { 56 | ctx.patchState({ 57 | token, 58 | username: action.payload.email, 59 | }); 60 | }); 61 | } 62 | 63 | @Action(Logout) 64 | logout(ctx: StateContext) { 65 | ctx.setState(INITIAL_STATE); 66 | ctx.dispatch(new Navigate(['/', PATH.root.auth, PATH.auth.signIn])); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/auth/sign-in/sign-in.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/sign-in/sign-in.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | 3 | a.blue-link { 4 | color: #548eaa; 5 | cursor: pointer; 6 | &:hover { 7 | color: #487284; 8 | text-decoration: underline; 9 | } 10 | } 11 | 12 | .error-message { 13 | padding: 20px 0; 14 | } 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/auth/sign-in/sign-in.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { SignInFormModel } from './sign-in.model'; 4 | import { PATH } from '../../models/routes.model'; 5 | import { AuthService } from '../auth.service'; 6 | 7 | @Component({ 8 | selector: 'blr-sign-in', 9 | templateUrl: './sign-in.component.html', 10 | styleUrls: ['./sign-in.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class SignInComponent { 14 | public formGroup = this.formBuilder.group({ 15 | email: null, 16 | password: null, 17 | }); 18 | 19 | public loadingSg = signal(false); 20 | 21 | public readonly FORGOT_PASSWORD_LINK = `/${PATH.root.auth}/${PATH.auth.forgotPassword}`; 22 | public readonly SIGNUP_LINK = `/${PATH.root.auth}/${PATH.auth.signUp}`; 23 | 24 | constructor( 25 | private authService: AuthService, 26 | private formBuilder: FormBuilder 27 | ) { 28 | this.formGroup.controls.email.setValidators([ 29 | Validators.required, 30 | Validators.email, 31 | ]); 32 | this.formGroup.controls.password.setValidators([Validators.required]); 33 | } 34 | 35 | submit() { 36 | const value: SignInFormModel = this.formGroup.getRawValue(); 37 | 38 | if (this.formGroup.invalid) { 39 | this.formGroup.setErrors({ formError: 'E-mail or password is wrong.' }); 40 | return; 41 | } 42 | this.authService.login(value.email, value.password); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/auth/sign-in/sign-in.model.ts: -------------------------------------------------------------------------------- 1 | export interface SignInFormModel { 2 | email: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/auth/sign-up/sign-up.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/sign-up/sign-up.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/app/auth/sign-up/sign-up.component.scss -------------------------------------------------------------------------------- /src/app/auth/sign-up/sign-up.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { SignUpFormModel } from './sign-up.model'; 4 | import { PATH } from '../../models/routes.model'; 5 | import { AuthService } from '../auth.service'; 6 | import { mustMatch } from '../../helpers/password-validators'; 7 | 8 | @Component({ 9 | selector: 'sign-up', 10 | templateUrl: './sign-up.component.html', 11 | styleUrls: ['./sign-up.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | }) 14 | export class SignUpComponent { 15 | public formGroup = this.formBuilder.group( 16 | { 17 | email: null, 18 | password: null, 19 | passwordConfirmation: null, 20 | }, 21 | { validators: mustMatch('password', 'passwordConfirmation') } 22 | ); 23 | 24 | public loadingSg = signal(false); 25 | 26 | public readonly SIGNIN_LINK = `/${PATH.root.auth}/${PATH.auth.signIn}`; 27 | 28 | constructor( 29 | private authService: AuthService, 30 | private formBuilder: FormBuilder 31 | ) { 32 | this.formGroup.controls.email.setValidators([ 33 | Validators.required, 34 | Validators.email, 35 | ]); 36 | this.formGroup.controls.password.setValidators([Validators.required]); 37 | } 38 | 39 | submit() { 40 | if (this.formGroup.invalid) { 41 | return; 42 | } 43 | const value: SignUpFormModel = this.formGroup.getRawValue(); 44 | this.loadingSg.set(true); 45 | this.authService.register(value.email, value.password); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/auth/sign-up/sign-up.model.ts: -------------------------------------------------------------------------------- 1 | export interface SignUpFormModel { 2 | email: string; 3 | password: string; 4 | passwordConfirmation: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/helpers/browser-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export enum EStorageKeys { 4 | TOKEN = 'access_token', 5 | COMMON_PHRASES = 'common_phrases', 6 | INVESTIGATORS = 'investigators', 7 | CASES = 'cases', 8 | BUSINESS = 'business', 9 | ADMINS = 'admins', 10 | CLIENTS = 'clients', 11 | REPORTS = 'reports', 12 | } 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class BrowserStorageService { 18 | // SESSION STORAGE 19 | 20 | public addToSessionStorage(key: EStorageKeys, data: any) { 21 | sessionStorage.setItem(key, JSON.stringify(data)); 22 | } 23 | 24 | public getFromSessionStorage(key: EStorageKeys) { 25 | const data = sessionStorage.getItem(key); 26 | return JSON.parse(data) as TModel; 27 | } 28 | 29 | public clearSessionStorage(keys: EStorageKeys[] = null) { 30 | if (keys === null) { 31 | sessionStorage.clear(); 32 | } else { 33 | keys.forEach(key => sessionStorage.removeItem(key)); 34 | } 35 | } 36 | 37 | // LOCAL STORAGE 38 | 39 | public addToLocalStorage(key: EStorageKeys, data: any) { 40 | localStorage.setItem(key, JSON.stringify(data)); 41 | } 42 | 43 | public getFromLocalStorage(key: EStorageKeys) { 44 | const data = localStorage.getItem(key); 45 | return JSON.parse(data) as TModel; 46 | } 47 | 48 | public clearLocalStorage(keys: EStorageKeys[] = null) { 49 | if (keys === null) { 50 | localStorage.clear(); 51 | } else { 52 | keys.forEach(key => localStorage.removeItem(key)); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/helpers/password-validators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractControl, 3 | FormGroup, 4 | ValidationErrors, 5 | ValidatorFn, 6 | } from '@angular/forms'; 7 | 8 | export function mustMatch( 9 | controlName: string, 10 | matchingControlName: string 11 | ): ValidatorFn { 12 | return (formGroup: AbstractControl): ValidationErrors | null => { 13 | if (!(formGroup instanceof FormGroup)) { 14 | throw new Error('mustMatch can be used with FormGroup only'); 15 | } 16 | const control = formGroup.controls[controlName]; 17 | const matchingControl = formGroup.controls[matchingControlName]; 18 | if (!control || !matchingControl) { 19 | throw new Error('formGroup has no requested controls'); 20 | } 21 | 22 | if (matchingControl.errors && !matchingControl.errors['mustMatch']) { 23 | return null; 24 | } 25 | 26 | if (control.value !== matchingControl.value) { 27 | matchingControl.setErrors({ mustMatch: true }); 28 | } else { 29 | matchingControl.setErrors(null); 30 | } 31 | return null; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | Home page -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/app/home/home.component.scss -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'home', 5 | templateUrl: './home.component.html', 6 | styleUrl: './home.component.scss', 7 | }) 8 | export class HomeComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | import { CommonModule } from '@angular/common'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | exports: [], 9 | declarations: [HomeComponent], 10 | providers: [], 11 | }) 12 | export class HomeModule {} 13 | -------------------------------------------------------------------------------- /src/app/models/routes.model.ts: -------------------------------------------------------------------------------- 1 | export const PATH = { 2 | root: { 3 | auth: 'auth', 4 | modal: 'modal', 5 | }, 6 | auth: { 7 | signIn: 'sign-in', 8 | signUp: 'sign-up', 9 | forgotPassword: 'forgot-password', 10 | resetPassword: 'reset-password', 11 | resetPasswordConfirm: 'reset-password-confirm', 12 | signUpConfirm: 'sign-up-confirm', 13 | confirmEmail: 'confirm-email', 14 | }, 15 | main: { 16 | admin: 'admin', 17 | users: 'users', 18 | } 19 | } 20 | 21 | export const MODAL_PATH = { 22 | deleteConfirm: 'delete', 23 | // CancelConfirm: 'cancel-confirm', 24 | // ContactForm: 'contact-modal', 25 | // Business: 'business-modal', 26 | // CasesForm: 'cases-modal', 27 | // AddInvestigators: 'add-investigators-modal', 28 | // EditPhoto: 'edit-photo', 29 | // CommonPhrasesForm: 'common-phrases-modal' 30 | } 31 | -------------------------------------------------------------------------------- /src/app/services/error-handler.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ErrorHandlerService { 9 | handle(error: HttpErrorResponse, caught: Observable): Observable { 10 | //add toaster and logging 11 | if (error.error instanceof Error) { 12 | // A client-side or network error occurred. Handle it accordingly. 13 | console.error('An error occurred:', error.error.message); 14 | } else { 15 | // The backend returned an unsuccessful response code. 16 | // The response body may contain clues as to what went wrong, 17 | console.error( 18 | `Backend returned code ${error.status}, body was: ${error.error}` 19 | ); 20 | } 21 | throw error; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/app/shared/footer/footer.component.scss -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'main-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrl: './footer.component.scss', 7 | standalone: true, 8 | }) 9 | export class FooterComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isAuthorized$ | async) { 4 | 11 | 12 | } 13 |
-------------------------------------------------------------------------------- /src/app/shared/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | padding: 20px 80px; 4 | 5 | &__nav-list { 6 | list-style: none; 7 | margin: 0; 8 | } 9 | } -------------------------------------------------------------------------------- /src/app/shared/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { AuthService } from '../../auth/auth.service'; 6 | 7 | @Component({ 8 | selector: 'main-header', 9 | templateUrl: './header.component.html', 10 | styleUrl: './header.component.scss', 11 | standalone: true, 12 | imports: [CommonModule, RouterModule, TranslateModule], 13 | }) 14 | export class HeaderComponent implements OnInit { 15 | public isAuthorized$ = this.authService.isAuthorized$; 16 | 17 | constructor(private authService: AuthService) {} 18 | 19 | ngOnInit() {} 20 | 21 | logout(): void { 22 | this.authService.logout(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preloader/preloader.component'; 2 | -------------------------------------------------------------------------------- /src/app/shared/preloader/preloader.component.html: -------------------------------------------------------------------------------- 1 | ... 2 | -------------------------------------------------------------------------------- /src/app/shared/preloader/preloader.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/app/shared/preloader/preloader.component.scss -------------------------------------------------------------------------------- /src/app/shared/preloader/preloader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'preloader', 5 | templateUrl: './preloader.component.html', 6 | styleUrl: './preloader.component.scss', 7 | standalone: true, 8 | }) 9 | export class PreloaderComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "AUTH": { 3 | "TITLE": "Translated value", 4 | "EMAIL": "E-mail", 5 | "PASSWORD": "Password", 6 | "PASSWORD_CONFIRMATION": "Confirm password", 7 | "SIGNUP": "Create Account", 8 | "SIGNIN": "Login", 9 | "SIGNIN_LINK": "Have you already had an account?", 10 | "FORGOT_PASSWORD": "Forgot your password?" 11 | }, 12 | "HEADER": { 13 | "NAVIGATION": { 14 | "HOME": "Home" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/assets/styles/media.scss: -------------------------------------------------------------------------------- 1 | $tablet: 768px; //tablet 2 | $laptop: 1200px; //pc, laptop 3 | $mac: 1400px; // mac 4 | $desktop: 1600px; //pc (*optional) 5 | $lagre: 1900px; //large view port width pc (*optional) 6 | 7 | 8 | @mixin tablet { 9 | @media only screen and (min-width: $tablet) { 10 | @content; 11 | } 12 | } 13 | 14 | @mixin tablet-landscape { 15 | @media only screen and (min-width: $tablet) and (orientation: landscape) { 16 | @content; 17 | } 18 | } 19 | 20 | @mixin laptop { 21 | @media only screen and (min-width: $laptop) { 22 | @content; 23 | } 24 | } 25 | 26 | @mixin mac { 27 | @media only screen and (min-width: 1350px) and (max-width: 1700px) { 28 | @content; 29 | } 30 | } 31 | 32 | @mixin desktop { 33 | @media only screen and (min-width: $desctop) { 34 | @content; 35 | } 36 | } 37 | 38 | @mixin lagre { 39 | @media only screen and (min-width: $lagre) { 40 | @content; 41 | } 42 | } -------------------------------------------------------------------------------- /src/assets/styles/normalize.scss: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // Normalize.scss settings 3 | // ============================================================================= 4 | 5 | 6 | // Set to true if you want to add support for IE6 and IE7 7 | // Notice: setting to true might render some elements 8 | // slightly differently than when set to false 9 | $legacy_support_for_ie: false !default; // Used also in Compass 10 | 11 | 12 | // Set the default font family here so you don't have to override it later 13 | $normalized_font_family: sans-serif !default; 14 | 15 | $normalize_headings: true !default; 16 | 17 | $h1_font_size: 2em !default; 18 | $h2_font_size: 1.5em !default; 19 | $h3_font_size: 1.17em !default; 20 | $h4_font_size: 1em !default; 21 | $h5_font_size: 0.83em !default; 22 | $h6_font_size: 0.75em !default; 23 | 24 | $h1_margin: 0.67em 0 !default; 25 | $h2_margin: 0.83em 0 !default; 26 | $h3_margin: 1em 0 !default; 27 | $h4_margin: 1.33em 0 !default; 28 | $h5_margin: 1.67em 0 !default; 29 | $h6_margin: 2.33em 0 !default; 30 | 31 | $background: #fff !default; 32 | $color: #000 !default; 33 | 34 | // ============================================================================= 35 | // HTML5 display definitions 36 | // ============================================================================= 37 | 38 | // Corrects block display not defined in IE6/7/8/9 & FF3 39 | 40 | article, 41 | aside, 42 | details, 43 | figcaption, 44 | figure, 45 | footer, 46 | header, 47 | hgroup, 48 | nav, 49 | section, 50 | summary { 51 | display: block; 52 | } 53 | 54 | // Corrects inline-block display not defined in IE6/7/8/9 & FF3 55 | 56 | audio, 57 | canvas, 58 | video { 59 | display: inline-block; 60 | @if $legacy_support_for_ie { 61 | *display: inline; 62 | *zoom: 1; 63 | } 64 | } 65 | 66 | // 1. Prevents modern browsers from displaying 'audio' without controls 67 | // 2. Remove excess height in iOS5 devices 68 | 69 | audio:not([controls]) { 70 | display: none; // 1 71 | height: 0; // 2 72 | } 73 | 74 | // 75 | // Address `[hidden]` styling not present in IE 8/9. 76 | // Hide the `template` element in IE, Safari, and Firefox < 22. 77 | // 78 | 79 | [hidden], template { 80 | display: none; 81 | } 82 | 83 | // ============================================================================= 84 | // Base 85 | // ============================================================================= 86 | 87 | // 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units 88 | // http://clagnut.com/blog/348/#c790 89 | // 2. Prevents iOS text size adjust after orientation change, without disabling user zoom 90 | // www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ 91 | 92 | html { 93 | @if $legacy_support_for_ie { 94 | font-size: 100%; // 1 95 | } 96 | background: $background; 97 | color: $color; 98 | -webkit-text-size-adjust: 100%; // 2 99 | -ms-text-size-adjust: 100%; // 2 100 | } 101 | 102 | // Addresses font-family inconsistency between 'textarea' and other form elements. 103 | 104 | html, 105 | button, 106 | input, 107 | select, 108 | textarea { 109 | font-family: $normalized_font_family; 110 | } 111 | 112 | // Addresses margins handled incorrectly in IE6/7 113 | 114 | body { 115 | margin: 0; 116 | } 117 | 118 | // ============================================================================= 119 | // Links 120 | // ============================================================================= 121 | 122 | // 1. Remove the gray background color from active links in IE 10. 123 | // 2. Addresses outline displayed oddly in Chrome 124 | // 3. Improves readability when focused and also mouse hovered in all browsers 125 | // people.opera.com/patrickl/experiments/keyboard/test 126 | 127 | a { 128 | // 1 129 | 130 | background: transparent; 131 | 132 | // 2 133 | 134 | &:focus { 135 | outline: thin dotted; 136 | } 137 | 138 | // 3 139 | 140 | &:hover, 141 | &:active { 142 | outline: 0; 143 | } 144 | } 145 | 146 | // ============================================================================= 147 | // Typography 148 | // ============================================================================= 149 | 150 | // Addresses font sizes and margins set differently in IE6/7 151 | // Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 152 | 153 | @if $normalize_headings == true { 154 | h1 { 155 | font-size: $h1_font_size; 156 | margin: $h1_margin; 157 | } 158 | 159 | h2 { 160 | font-size: $h2_font_size; 161 | margin: $h2_margin; 162 | } 163 | 164 | h3 { 165 | font-size: $h3_font_size; 166 | margin: $h3_margin; 167 | } 168 | 169 | h4 { 170 | font-size: $h4_font_size; 171 | margin: $h4_margin; 172 | } 173 | 174 | h5 { 175 | font-size: $h5_font_size; 176 | margin: $h5_margin; 177 | } 178 | 179 | h6 { 180 | font-size: $h6_font_size; 181 | margin: $h6_margin; 182 | } 183 | } 184 | 185 | // Addresses styling not present in IE 8/9, S5, Chrome 186 | 187 | abbr[title] { 188 | border-bottom: 1px dotted; 189 | } 190 | 191 | // Addresses style set to 'bolder' in FF3+, S4/5, Chrome 192 | 193 | b, 194 | strong { 195 | font-weight: bold; 196 | } 197 | 198 | @if $legacy_support_for_ie { 199 | blockquote { 200 | margin: 1em 40px; 201 | } 202 | } 203 | 204 | // Addresses styling not present in S5, Chrome 205 | 206 | dfn { 207 | font-style: italic; 208 | } 209 | 210 | // Addresses styling not present in IE6/7/8/9 211 | 212 | mark { 213 | background: #ff0; 214 | color: #000; 215 | } 216 | 217 | // Addresses margins set differently in IE6/7 218 | @if $legacy_support_for_ie { 219 | p, 220 | pre { 221 | margin: 1em 0; 222 | } 223 | } 224 | 225 | // Corrects font family set oddly in IE6, S4/5, Chrome 226 | // en.wikipedia.org/wiki/User:Davidgothberg/Test59 227 | 228 | code, 229 | kbd, 230 | pre, 231 | samp { 232 | font-family: monospace, serif; 233 | @if $legacy_support_for_ie { 234 | _font-family: 'courier new', monospace; 235 | } 236 | font-size: 1em; 237 | } 238 | 239 | // Improves readability of pre-formatted text in all browsers 240 | 241 | pre { 242 | white-space: pre; 243 | white-space: pre-wrap; 244 | word-wrap: break-word; 245 | } 246 | 247 | // Set consistent quote types. 248 | 249 | q { 250 | quotes: "\201C" "\201D" "\2018" "\2019"; 251 | } 252 | 253 | // 1. Addresses CSS quotes not supported in IE6/7 254 | // 2. Addresses quote property not supported in S4 255 | 256 | // 1 257 | @if $legacy_support_for_ie { 258 | q { 259 | quotes: none; 260 | } 261 | } 262 | 263 | // 2 264 | q { 265 | &:before, 266 | &:after { 267 | content: ''; 268 | content: none; 269 | } 270 | } 271 | 272 | // Address inconsistent and variable font size in all browsers. 273 | 274 | small { 275 | font-size: 80%; 276 | } 277 | 278 | // Prevents sub and sup affecting line-height in all browsers 279 | // gist.github.com/413930 280 | 281 | sub, 282 | sup { 283 | font-size: 75%; 284 | line-height: 0; 285 | position: relative; 286 | vertical-align: baseline; 287 | } 288 | 289 | sup { 290 | top: -0.5em; 291 | } 292 | 293 | sub { 294 | bottom: -0.25em; 295 | } 296 | 297 | // ============================================================================= 298 | // Lists 299 | // ============================================================================= 300 | 301 | // Addresses margins set differently in IE6/7 302 | @if $legacy_support_for_ie { 303 | dl, 304 | menu, 305 | ol, 306 | ul { 307 | margin: 1em 0; 308 | } 309 | } 310 | 311 | @if $legacy_support_for_ie { 312 | dd { 313 | margin: 0 0 0 40px; 314 | } 315 | } 316 | 317 | // Addresses paddings set differently in IE6/7 318 | @if $legacy_support_for_ie { 319 | menu, 320 | ol, 321 | ul { 322 | padding: 0 0 0 40px; 323 | } 324 | } 325 | 326 | // Corrects list images handled incorrectly in IE7 327 | 328 | nav { 329 | ul, 330 | ol { 331 | @if $legacy_support_for_ie { 332 | list-style-image: none; 333 | } 334 | } 335 | } 336 | 337 | // ============================================================================= 338 | // Embedded content 339 | // ============================================================================= 340 | 341 | // 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 342 | // 2. Improves image quality when scaled in IE7 343 | // code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ 344 | 345 | img { 346 | border: 0; // 1 347 | @if $legacy_support_for_ie { 348 | -ms-interpolation-mode: bicubic; // 2 349 | } 350 | } 351 | 352 | // Corrects overflow displayed oddly in IE9 353 | 354 | svg:not(:root) { 355 | overflow: hidden; 356 | } 357 | 358 | // ============================================================================= 359 | // Figures 360 | // ============================================================================= 361 | 362 | // Addresses margin not present in IE6/7/8/9, S5, O11 363 | 364 | figure { 365 | margin: 0; 366 | } 367 | 368 | // ============================================================================= 369 | // Forms 370 | // ============================================================================= 371 | 372 | // Corrects margin displayed oddly in IE6/7 373 | @if $legacy_support_for_ie { 374 | form { 375 | margin: 0; 376 | } 377 | } 378 | 379 | // Define consistent border, margin, and padding 380 | 381 | fieldset { 382 | border: 1px solid #c0c0c0; 383 | margin: 0 2px; 384 | padding: 0.35em 0.625em 0.75em; 385 | } 386 | 387 | // 1. Corrects color not being inherited in IE6/7/8/9 388 | // 2. Remove padding so people aren't caught out if they zero out fieldsets. 389 | // 3. Corrects text not wrapping in FF3 390 | // 4. Corrects alignment displayed oddly in IE6/7 391 | 392 | legend { 393 | border: 0; // 1 394 | padding: 0; // 2 395 | white-space: normal; // 3 396 | @if $legacy_support_for_ie { 397 | *margin-left: -7px; // 4 398 | } 399 | } 400 | 401 | // 1. Correct font family not being inherited in all browsers. 402 | // 2. Corrects font size not being inherited in all browsers 403 | // 3. Addresses margins set differently in IE6/7, FF3+, S5, Chrome 404 | // 4. Improves appearance and consistency in all browsers 405 | 406 | button, 407 | input, 408 | select, 409 | textarea { 410 | font-family: inherit; // 1 411 | font-size: 100%; // 2 412 | margin: 0; // 3 413 | vertical-align: baseline; // 4 414 | @if $legacy_support_for_ie { 415 | *vertical-align: middle; // 4 416 | } 417 | } 418 | 419 | // Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet 420 | 421 | button, input { 422 | line-height: normal; 423 | } 424 | 425 | // Address inconsistent `text-transform` inheritance for `button` and `select`. 426 | // All other form control elements do not inherit `text-transform` values. 427 | // Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 428 | // Correct `select` style inheritance in Firefox 4+ and Opera. 429 | 430 | button, 431 | select { 432 | text-transform: none; 433 | } 434 | 435 | // 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 436 | // and `video` controls 437 | // 2. Corrects inability to style clickable 'input' types in iOS 438 | // 3. Improves usability and consistency of cursor style between image-type 439 | // 'input' and others 440 | // 4. Removes inner spacing in IE7 without affecting normal text inputs 441 | // Known issue: inner spacing remains in IE6 442 | 443 | button, 444 | html input[type="button"], // 1 445 | input[type="reset"], 446 | input[type="submit"] { 447 | -webkit-appearance: button; // 2 448 | cursor: pointer; // 3 449 | @if $legacy_support_for_ie { 450 | *overflow: visible; // 4 451 | } 452 | } 453 | 454 | // Re-set default cursor for disabled elements 455 | 456 | button[disabled], 457 | input[disabled] { 458 | cursor: default; 459 | } 460 | 461 | // Removes inner padding and border in FF3+ 462 | // www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ 463 | 464 | button, input { 465 | &::-moz-focus-inner { 466 | border: 0; 467 | padding: 0; 468 | } 469 | } 470 | 471 | // 1. Removes default vertical scrollbar in IE6/7/8/9 472 | // 2. Improves readability and alignment in all browsers 473 | 474 | textarea { 475 | overflow: auto; // 1 476 | vertical-align: top; // 2 477 | } 478 | 479 | // ============================================================================= 480 | // Tables 481 | // ============================================================================= 482 | 483 | // Remove most spacing between table cells 484 | 485 | table { 486 | border-collapse: collapse; 487 | border-spacing: 0; 488 | } 489 | 490 | input { 491 | // 1. Addresses appearance set to searchfield in S5, Chrome 492 | // 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) 493 | &[type="search"] { 494 | -webkit-appearance: textfield; // 1 495 | -moz-box-sizing: content-box; 496 | -webkit-box-sizing: content-box; // 2 497 | box-sizing: content-box; 498 | 499 | // Remove inner padding and search cancel button in Safari 5 and Chrome 500 | // on OS X. 501 | &::-webkit-search-cancel-button, 502 | &::-webkit-search-decoration { 503 | -webkit-appearance: none; 504 | } 505 | } 506 | 507 | // 1. Address box sizing set to `content-box` in IE 8/9/10. 508 | // 2. Remove excess padding in IE 8/9/10. 509 | // 3. Removes excess padding in IE7 510 | // Known issue: excess padding remains in IE6 511 | &[type="checkbox"], 512 | &[type="radio"] { 513 | box-sizing: border-box; // 1 514 | padding: 0; // 2 515 | @if $legacy_support_for_ie { 516 | *height: 13px; // 3 517 | *width: 13px; // 3 518 | } 519 | } 520 | } -------------------------------------------------------------------------------- /src/assets/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #000000; 2 | $error-color: #a31313; -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | baseUrl: 'https://boilerplateapi.stage.sharp-dev.net/api', 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | baseUrl: 'https://boilerplateapi.stage.sharp-dev.net/api', 4 | locales: ['en'], 5 | defaultLocale: 'en', 6 | }; 7 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vallettasoftware/boilerplate-angular/d5f43137184b18b93cd0f8bd73f264eacf76b96c/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PromoBoilerplateAngular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./assets/styles/normalize.scss"; 2 | @import "./assets/styles/variables.scss"; 3 | 4 | // FORMS 5 | input.ng-invalid.ng-dirty { 6 | border-color: $error-color; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "strictNullChecks": false, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "useDefineForClassFields": false, 23 | "lib": [ 24 | "ES2022", 25 | "dom" 26 | ] 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------