├── .editorconfig ├── .firebase └── hosting.ZGlzdFxicm93c2Vy.cache ├── .firebaserc ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── firebase.json ├── karma.conf.js ├── ngsw-config.json ├── package-lock.json ├── package.json ├── prerender.js ├── serve-script.js ├── server.ts ├── server ├── app.module.ts ├── main.ts └── tsconfig.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.server.module.ts │ ├── customers │ │ ├── customer-data.service.ts │ │ ├── customers-routing.module.ts │ │ ├── customers.module.ts │ │ ├── detail-page │ │ │ ├── detail-page.component.html │ │ │ ├── detail-page.component.scss │ │ │ └── detail-page.component.ts │ │ └── list-page │ │ │ ├── list-page.component.html │ │ │ ├── list-page.component.scss │ │ │ └── list-page.component.ts │ ├── home-page │ │ ├── home-page.component.html │ │ ├── home-page.component.scss │ │ └── home-page.component.ts │ ├── kanban │ │ ├── board.model.ts │ │ ├── board.service.ts │ │ ├── board │ │ │ ├── board.component.html │ │ │ ├── board.component.scss │ │ │ └── board.component.ts │ │ ├── boards-list │ │ │ ├── boards-list.component.html │ │ │ ├── boards-list.component.scss │ │ │ └── boards-list.component.ts │ │ ├── dialogs │ │ │ ├── board-dialog.component.ts │ │ │ ├── dialog.scss │ │ │ └── task-dialog.component.ts │ │ ├── kanban-routing.module.ts │ │ └── kanban.module.ts │ ├── services │ │ ├── seo.service.ts │ │ └── snack.service.ts │ ├── shared │ │ ├── delete-button │ │ │ ├── delete-button.component.html │ │ │ ├── delete-button.component.scss │ │ │ └── delete-button.component.ts │ │ ├── shared.module.ts │ │ └── shell │ │ │ ├── shell.component.html │ │ │ ├── shell.component.scss │ │ │ └── shell.component.ts │ └── user │ │ ├── auth.guard.ts │ │ ├── email-login │ │ ├── email-login.component.html │ │ ├── email-login.component.scss │ │ └── email-login.component.ts │ │ ├── google-signin.directive.ts │ │ ├── login-page │ │ ├── login-page.component.html │ │ ├── login-page.component.scss │ │ └── login-page.component.ts │ │ ├── user-routing.module.ts │ │ └── user.module.ts ├── assets │ ├── .gitkeep │ ├── default-user.svg │ ├── google-logo.svg │ ├── google-signin.png │ └── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.server.ts ├── main.ts ├── manifest.webmanifest ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.server.json ├── tsconfig.spec.json ├── tslint.json └── webpack.server.config.js /.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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.firebase/hosting.ZGlzdFxicm93c2Vy.cache: -------------------------------------------------------------------------------- 1 | 5-es2015.c55fcfaf07421a89a28b.js,1567704753109,cfc93ba80e072538f9fc3e7ebc538c44df5d907a5f658b3cd98a1fa314122348 2 | 5-es5.c55fcfaf07421a89a28b.js,1567704753138,7c883131d34bf24d2db9939534effa65170f56ea847b4328a83d988b3d4cf0a4 3 | 7-es2015.70b426c1fa190b93a375.js,1567704752707,9ed87f9036a06e70151fe3de1242cdc4548cfd94ba1fff238d8a880c7dc7596d 4 | 7-es5.70b426c1fa190b93a375.js,1567704752720,9ed87f9036a06e70151fe3de1242cdc4548cfd94ba1fff238d8a880c7dc7596d 5 | favicon.ico,1567704750329,2c19690e9587bae12f419b34d2edeecc76808099540a9c9f4ea6194116cfc8f7 6 | index.html,1567704767688,90e32a05e6368fbcf2f03a1227624de5574e39fc572710c8946237adbc4949c1 7 | 3rdpartylicenses.txt,1567704750329,5adcf5129789998a65da9d0c9283ee34bb849eceb5e6a552a7f3d3ce38ff0ad6 8 | manifest.webmanifest,1567704750329,e0f0cd22dd42bfc556f2fe9e15d1012a7afeb1abbb934f3becc2763eac4e601b 9 | ngsw.json,1567704767724,9aae1a5ce3944ee028b402ac260b0c080718cc34a2c1afb4228ecbaf05e630cf 10 | runtime-es2015.8ad244abf983cc11f6ff.js,1567704752717,81e4ce3c65a961d421fc5d018487089b6ec52d26090a2800fdcc9007ca97ecd5 11 | runtime-es5.8ad244abf983cc11f6ff.js,1567704752742,1c4d066ac18bd0492dccca7a73e85b43bd2d724ab0506a67861dcf55c0e7b6d6 12 | safety-worker.js,1567704767827,40226cabd4735f93fc57c887bdb3ed6e6930cb6b1c986d6a640758edaf490050 13 | polyfills-es2015.fd917e7c3ed57f282ee5.js,1567704751372,597aea6bf09f25be57228b80f045822d8676487b3d34889cb9ac784dc50a0ac5 14 | worker-basic.min.js,1567704767827,40226cabd4735f93fc57c887bdb3ed6e6930cb6b1c986d6a640758edaf490050 15 | assets/default-user.svg,1567704750345,604bf25ff42d93a0eafe451cd0fd98d68fa29e6634dd33f2451b447984c84f8d 16 | assets/google-logo.svg,1567704750353,81b4558cae92c6ea861bd86726b64a299af05df442220bf458e008239a9f5f99 17 | assets/google-signin.png,1567704750353,ebaaaecae3bbbbb161da8682fb1289b6cbe692edd95dc4e419def00e40622509 18 | assets/icons/icon-128x128.png,1567704750353,1331bbcee85eb4c732d2b2468c0118ead141aa56c741dc1a93a54d4812e79d4d 19 | assets/icons/icon-144x144.png,1567704750362,ab4052ca6640d9378a090b7a6825df604c939ec738dae88d89500aee8eb7087f 20 | assets/icons/icon-152x152.png,1567704750362,b1cb2eb328c9eedac5eb3994d04c73040c462a73cf7254c38eafa2d582a078d6 21 | assets/icons/icon-192x192.png,1567704750362,5b2471e9386f4fe3dcf1d2151c7048c3cc22d3c504161debeca3af3a2efd0a04 22 | assets/icons/icon-384x384.png,1567704750362,e28ae36bf6b6f9c9a5eb8716806f3077d2932c9723f89d0c0275fbde43dcdce9 23 | assets/icons/icon-512x512.png,1567704750362,cb62ee904d0aa462c04667e945abbefe928c1c874d9a919ec237f3ebc1822dc9 24 | assets/icons/icon-96x96.png,1567704750362,5dbec692238f9b1305461df3fe87c910306d95db26941804b8929cd0d99bd508 25 | assets/icons/icon-72x72.png,1567704750362,215a8d62891dff36be3e0a554c28e627bbd34dea95aca21b718414452b581dce 26 | styles.2111818059348b84f0cf.css,1567704750329,1d0e68b7ddadd10d13d9d58bb8645b84c98af21b6108f9c88e9e565cbc1b107e 27 | 6-es5.56501b254feef8e3588d.js,1567704754571,12223760f51a4d03d4c298de3c8fed2e5715f5402ce62e600b7d044a358726f9 28 | 6-es2015.56501b254feef8e3588d.js,1567704754492,50d9fdef4436f1766cd6215ca837974ac7655e1d7d011131e4abf9b91af04351 29 | polyfills-es5.3aa54d3e5134f5b5b842.js,1567704754969,bffb375e8b3eaa35f44e5e6925a85aba1800d74f16e82f4e6fcc0199959df5fc 30 | ngsw-worker.js,1567704767776,dc4947600b0957d8ed0e09fe107ca3f44621ad27f0d7655b61d92c618ab441c8 31 | main-es2015.5dedd333a5f38d423170.js,1567704766807,97fdbe8d07765184ccddc27d6f13fd18715b499de55f6517bed97ff81ac8e7f8 32 | main-es5.5dedd333a5f38d423170.js,1567704767315,3b1624484b7897ff90118f1fb6d020d23ed0a2629190694a4a8a083558c8105a 33 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "angular-voxer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | /src/env.ts 4 | /.firebase 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | # Only exists if Bazel was run 11 | /bazel-out 12 | 13 | # dependencies 14 | /node_modules 15 | 16 | # profiling files 17 | chrome-profiler-events*.json 18 | speed-measure-plugin*.json 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | .history/* 36 | 37 | # misc 38 | /.sass-cache 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:10 3 | 4 | WORKDIR usr/src/app 5 | 6 | # ENV PORT 8080 7 | # ENV HOST 0.0.0.0 8 | 9 | COPY package*.json ./ 10 | RUN npm install 11 | 12 | # Copy local angular/nest code to the container 13 | COPY . . 14 | 15 | # Build production app 16 | RUN npm run build:ssr 17 | 18 | # EXPOSE 4200 19 | 20 | CMD ["npm", "run", "serve:ssr"] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FireStarter 2 | 3 | Firestarter is an Angular PWA powered by Firebase. 4 | 5 | - [Live Demo](https://firestarter.fireship.io/) 6 | - [Lessons and Screencasts](https://fireship.io) 7 | - [Join the Slack Team](https://fireship.page.link/slack) 8 | 9 | ![](https://firebasestorage.googleapis.com/v0/b/angular-voxer.appspot.com/o/demo-gif.gif?alt=media&token=dadcdb72-eb58-4903-b6b8-c741c27a08c4) 10 | 11 | ## Features 12 | 13 | - Angular 12.x + Firebase 14 | - Installable PWA 15 | - OAuth and Email/Password Signup with Firebase 16 | - Drag & drop Kanban demo with Firestore 17 | - Angular Universal SSR with Nest.js deployed to Google Cloud Run 18 | - Optional SSR Prerendering Script 19 | 20 | ## Usage 21 | 22 | 1. Run 23 | 24 | - `git clone https://github.com/codediodeio/angular-firestarter.git firestarter` 25 | - `cd firestarter` 26 | - `npm install` 27 | 28 | 2. Create a project at https://firebase.google.com/ and grab your web config: 29 | 30 | ![](https://firebasestorage.googleapis.com/v0/b/firestarter-96e46.appspot.com/o/project-config.PNG?alt=media&token=5eabb205-7ba2-4fc3-905f-e9547055e754) 31 | 32 | 3. Add the config to your Angular environment 33 | 34 | #### src/environments/ 35 | 36 | Update the `environment.prod.ts` and `environment.ts` files. 37 | 38 | ```typescript 39 | export const environment = { 40 | production: false, 41 | firebase: { 42 | apiKey: 'APIKEY', 43 | authDomain: 'DEV-APP.firebaseapp.com', 44 | databaseURL: 'https://DEV-APP.firebaseio.com', 45 | projectId: 'DEV-APP', 46 | storageBucket: 'DEV-APP.appspot.com', 47 | messagingSenderId: '...', 48 | appId: '...', 49 | } 50 | }; 51 | ``` 52 | 53 | 54 | 5. Run `ng serve` 55 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "firestarter": { 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:browser", 19 | "options": { 20 | "outputPath": "dist/browser", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets", 29 | "src/manifest.webmanifest" 30 | ], 31 | "styles": [ 32 | "src/styles.scss" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | }, 58 | { 59 | "type": "anyComponentStyle", 60 | "maximumWarning": "6kb", 61 | "maximumError": "10kb" 62 | } 63 | ], 64 | "serviceWorker": true, 65 | "ngswConfigPath": "ngsw-config.json" 66 | } 67 | } 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "options": { 72 | "browserTarget": "firestarter:build" 73 | }, 74 | "configurations": { 75 | "production": { 76 | "browserTarget": "firestarter:build:production" 77 | } 78 | } 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "firestarter:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "polyfills": "src/polyfills.ts", 91 | "tsConfig": "tsconfig.spec.json", 92 | "karmaConfig": "karma.conf.js", 93 | "assets": [ 94 | "src/favicon.ico", 95 | "src/assets", 96 | "src/manifest.webmanifest" 97 | ], 98 | "styles": [ 99 | "src/styles.scss" 100 | ], 101 | "scripts": [] 102 | } 103 | }, 104 | "lint": { 105 | "builder": "@angular-devkit/build-angular:tslint", 106 | "options": { 107 | "tsConfig": [ 108 | "tsconfig.app.json", 109 | "tsconfig.spec.json", 110 | "e2e/tsconfig.json" 111 | ], 112 | "exclude": [ 113 | "**/node_modules/**" 114 | ] 115 | } 116 | }, 117 | "e2e": { 118 | "builder": "@angular-devkit/build-angular:protractor", 119 | "options": { 120 | "protractorConfig": "e2e/protractor.conf.js", 121 | "devServerTarget": "firestarter:serve" 122 | }, 123 | "configurations": { 124 | "production": { 125 | "devServerTarget": "firestarter:serve:production" 126 | } 127 | } 128 | }, 129 | "server": { 130 | "builder": "@angular-devkit/build-angular:server", 131 | "options": { 132 | "outputPath": "dist/server", 133 | "main": "server.ts", 134 | "tsConfig": "tsconfig.server.json" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "fileReplacements": [ 139 | { 140 | "replace": "src/environments/environment.ts", 141 | "with": "src/environments/environment.prod.ts" 142 | } 143 | ] 144 | , "optimization": true 145 | } 146 | } 147 | }, 148 | "serve-ssr": { 149 | "builder": "@nguniversal/builders:ssr-dev-server", 150 | "options": { 151 | "browserTarget": "firestarter:build", 152 | "serverTarget": "firestarter:server" 153 | }, 154 | "configurations": { 155 | "production": { 156 | "browserTarget": "firestarter:build:production", 157 | "serverTarget": "firestarter:server:production" 158 | } 159 | } 160 | }, 161 | "prerender": { 162 | "builder": "@nguniversal/builders:prerender", 163 | "options": { 164 | "browserTarget": "firestarter:build:production", 165 | "serverTarget": "firestarter:server:production", 166 | "routes": [ 167 | "/" 168 | ] 169 | }, 170 | "configurations": { 171 | "production": {} 172 | } 173 | } 174 | } 175 | } 176 | }, 177 | "defaultProject": "firestarter" 178 | } -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('firestarter app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/browser", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "run": { 13 | "serviceId": "nest-angular-ssr", 14 | "region": "us-central1" 15 | } 16 | } 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/firestarter'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, { 18 | "name": "assets", 19 | "installMode": "lazy", 20 | "updateMode": "prefetch", 21 | "resources": { 22 | "files": [ 23 | "/assets/**", 24 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 25 | ] 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firestarter", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "serve": "node serve-script", 12 | "build:prerender": "npm run build:client-and-server-bundles && node prerender.js", 13 | "serve:prerender": "http-server dist/browser -c-1", 14 | "compile:server": "webpack --config webpack.server.config.js --progress --colors", 15 | "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", 16 | "serve:ssr": "node dist/server", 17 | "build:client-and-server-bundles": "ng build && ng run firestarter:server:production", 18 | "dev:ssr": "ng run firestarter:serve-ssr", 19 | "prerender": "ng run firestarter:prerender" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@angular/animations": "~12.0.1", 24 | "@angular/cdk": "~12.0.1", 25 | "@angular/common": "~12.0.1", 26 | "@angular/compiler": "~12.0.1", 27 | "@angular/core": "~12.0.1", 28 | "@angular/fire": "^6.1.5", 29 | "@angular/forms": "~12.0.1", 30 | "@angular/material": "^12.0.1", 31 | "@angular/platform-browser": "~12.0.1", 32 | "@angular/platform-browser-dynamic": "~12.0.1", 33 | "@angular/platform-server": "~12.0.1", 34 | "@angular/router": "~12.0.1", 35 | "@angular/service-worker": "~12.0.1", 36 | "@nestjs/common": "^6.11.6", 37 | "@nestjs/core": "^6.11.6", 38 | "@nestjs/ng-universal": "^3.0.0", 39 | "@nestjs/platform-express": "^6.11.6", 40 | "@nguniversal/express-engine": "^12.0.0", 41 | "express": "^4.15.2", 42 | "firebase": "^8.6.2", 43 | "reflect-metadata": "^0.1.13", 44 | "rxjs": "~6.5.4", 45 | "tslib": "^1.10.0", 46 | "zone.js": "^0.11.4" 47 | }, 48 | "devDependencies": { 49 | "@angular-devkit/architect": "0.1200.1", 50 | "@angular-devkit/build-angular": "~12.0.1", 51 | "@angular/cli": "~12.0.1", 52 | "@angular/compiler-cli": "~12.0.1", 53 | "@angular/language-service": "~12.0.1", 54 | "@nguniversal/builders": "^12.0.0", 55 | "@types/express": "^4.17.0", 56 | "@types/jasmine": "~3.5.3", 57 | "@types/jasminewd2": "~2.0.8", 58 | "@types/node": "^12.11.1", 59 | "codelyzer": "^6.0.2", 60 | "firebase-tools": "^7.13.0", 61 | "fuzzy": "^0.1.3", 62 | "inquirer": "^7.0.4", 63 | "inquirer-autocomplete-prompt": "^1.0.2", 64 | "jasmine-core": "~3.5.0", 65 | "jasmine-spec-reporter": "~4.2.1", 66 | "karma": "~4.4.1", 67 | "karma-chrome-launcher": "~3.1.0", 68 | "karma-coverage-istanbul-reporter": "~2.1.1", 69 | "karma-jasmine": "~3.1.1", 70 | "karma-jasmine-html-reporter": "^1.5.2", 71 | "nodemon": "^2.0.2", 72 | "protractor": "~5.4.3", 73 | "rimraf": "^3.0.2", 74 | "ts-loader": "^6.2.1", 75 | "ts-node": "~8.6.2", 76 | "tslint": "~6.0.0", 77 | "typescript": "^4.2.3", 78 | "wait-on": "^4.0.0", 79 | "webpack-cli": "^3.3.11" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /prerender.js: -------------------------------------------------------------------------------- 1 | require('zone.js/dist/zone-node'); 2 | require('reflect-metadata'); 3 | 4 | const { join } = require('path'); 5 | 6 | const { enableProdMode } = require('@angular/core'); 7 | 8 | // Import module map for lazy loading 9 | const { 10 | provideModuleMap 11 | } = require('@nguniversal/module-map-ngfactory-loader'); 12 | const { renderModuleFactory } = require('@angular/platform-server'); 13 | 14 | const fs = require('fs-extra'); 15 | 16 | // Must manually define routes to prerender 17 | const ROUTES = [ 18 | '/', 19 | '/customers', 20 | '/customers/78asJMXvM8q7f87cpVEF', 21 | '/customers/Wu2BRnrAxnizSgGaJXhN', 22 | '/customers/qe7EtWu4UWiWfZgtmP3C', 23 | '/kanban', 24 | '/login', 25 | ]; 26 | 27 | // leave this as require(), imported via webpack 28 | const { 29 | AppServerModuleNgFactory, 30 | LAZY_MODULE_MAP 31 | } = require(`./dist/server/main`); 32 | 33 | // START prerender script 34 | 35 | (async function() { 36 | enableProdMode(); 37 | // Get the app index 38 | const views = 'dist/browser'; 39 | const index = await fs.readFile(join(views, 'index.html'), 'utf8'); 40 | 41 | // Loop over each route 42 | for (const route of ROUTES) { 43 | const pageDir = join(views, route); 44 | await fs.ensureDir(pageDir); 45 | 46 | // Render with Universal 47 | const html = await renderModuleFactory(AppServerModuleNgFactory, { 48 | document: index, 49 | url: route, 50 | extraProviders: [provideModuleMap(LAZY_MODULE_MAP)] 51 | }); 52 | 53 | await fs.writeFile(join(pageDir, 'index.html'), html); 54 | } 55 | 56 | process.exit(); 57 | console.log('prerendering complete'); 58 | })(); 59 | -------------------------------------------------------------------------------- /serve-script.js: -------------------------------------------------------------------------------- 1 | const { LiveReloadCompiler } = require('@nestjs/ng-universal'); 2 | 3 | const compiler = new LiveReloadCompiler({ 4 | projectName: 'firestarter' 5 | }); 6 | compiler.run(); 7 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | 3 | import { ngExpressEngine } from '@nguniversal/express-engine'; 4 | import * as express from 'express'; 5 | import { join } from 'path'; 6 | 7 | import { AppServerModule } from './src/main.server'; 8 | import { APP_BASE_HREF } from '@angular/common'; 9 | import { existsSync } from 'fs'; 10 | 11 | // The Express app is exported so that it can be used by serverless Functions. 12 | export function app() { 13 | const server = express(); 14 | const distFolder = join(process.cwd(), 'dist/browser'); 15 | const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index'; 16 | 17 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 18 | server.engine('html', ngExpressEngine({ 19 | bootstrap: AppServerModule, 20 | })); 21 | 22 | server.set('view engine', 'html'); 23 | server.set('views', distFolder); 24 | 25 | // Example Express Rest API endpoints 26 | // app.get('/api/**', (req, res) => { }); 27 | // Serve static files from /browser 28 | server.get('*.*', express.static(distFolder, { 29 | maxAge: '1y' 30 | })); 31 | 32 | // All regular routes use the Universal engine 33 | server.get('*', (req, res) => { 34 | res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); 35 | }); 36 | 37 | return server; 38 | } 39 | 40 | function run() { 41 | const port = process.env.PORT || 4000; 42 | 43 | // Start up the Node server 44 | const server = app(); 45 | server.listen(port, () => { 46 | console.log(`Node Express server listening on http://localhost:${port}`); 47 | }); 48 | } 49 | 50 | // Webpack will replace 'require' with '__webpack_require__' 51 | // '__non_webpack_require__' is a proxy to Node 'require' 52 | // The below code is to ensure that the server is run only when not requiring the bundle. 53 | declare const __non_webpack_require__: NodeRequire; 54 | const mainModule = __non_webpack_require__.main; 55 | const moduleFilename = mainModule && mainModule.filename || ''; 56 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 57 | run(); 58 | } 59 | 60 | export * from './src/main.server'; 61 | 62 | export { renderModule, renderModuleFactory } from '@angular/platform-server'; -------------------------------------------------------------------------------- /server/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AngularUniversalModule } from '@nestjs/ng-universal'; 3 | import { join } from 'path'; 4 | 5 | @Module({ 6 | imports: [ 7 | AngularUniversalModule.forRoot({ 8 | viewsPath: join(process.cwd(), 'dist/browser'), 9 | bootstrap: ApplicationModule 10 | // bundle: require('../server/main'), 11 | // liveReload: true 12 | }) 13 | ] 14 | }) 15 | export class ApplicationModule {} 16 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { ApplicationModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(ApplicationModule); 6 | app.setGlobalPrefix('api'); 7 | await app.listen(process.env.PORT || 8080); 8 | } 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": true, 10 | "outDir": "../dist/server-app" 11 | }, 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HomePageComponent } from './home-page/home-page.component'; 4 | import { AuthGuard } from './user/auth.guard'; 5 | 6 | const routes: Routes = [ 7 | { path: '', component: HomePageComponent }, 8 | { 9 | path: 'login', 10 | loadChildren: () => import('./user/user.module').then(m => m.UserModule) 11 | }, 12 | { 13 | path: 'kanban', 14 | loadChildren: () => 15 | import('./kanban/kanban.module').then(m => m.KanbanModule), 16 | canActivate: [AuthGuard] 17 | }, 18 | { 19 | path: 'customers', 20 | loadChildren: () => 21 | import('./customers/customers.module').then(m => m.CustomersModule), 22 | } 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forRoot(routes, { 27 | initialNavigation: 'enabled' 28 | })], 29 | exports: [RouterModule] 30 | }) 31 | export class AppRoutingModule {} 32 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { environment } from '../environments/environment'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | // App Modules 7 | import { AppComponent } from './app.component'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { HomePageComponent } from './home-page/home-page.component'; 10 | import { SharedModule } from './shared/shared.module'; 11 | import { UserModule } from './user/user.module'; 12 | 13 | // Firebase imports 14 | import { AngularFireModule } from '@angular/fire'; 15 | import { AngularFirestoreModule } from '@angular/fire/firestore'; 16 | import { AngularFireAuthModule } from '@angular/fire/auth'; 17 | import { ServiceWorkerModule } from '@angular/service-worker'; 18 | 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent, 23 | HomePageComponent, 24 | ], 25 | imports: [ 26 | BrowserModule.withServerTransition({ appId: 'serverApp' }), 27 | AppRoutingModule, 28 | BrowserAnimationsModule, 29 | SharedModule, 30 | UserModule, 31 | AngularFireModule.initializeApp(environment.firebase), 32 | AngularFirestoreModule, 33 | AngularFireAuthModule, 34 | ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), 35 | ], 36 | providers: [], 37 | bootstrap: [AppComponent] 38 | }) 39 | export class AppModule { } 40 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | 4 | import { AppModule } from './app.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | AppModule, 10 | ServerModule 11 | ], 12 | bootstrap: [AppComponent], 13 | }) 14 | export class AppServerModule {} 15 | -------------------------------------------------------------------------------- /src/app/customers/customer-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFirestore } from '@angular/fire/firestore'; 3 | import { of } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class CustomerDataService { 9 | 10 | customers = null; 11 | subscription; 12 | 13 | subscribeToCustomers() { 14 | if (!this.customers) { 15 | this.subscription = this.db.collection('customers').valueChanges({idField: 'id'}) 16 | .subscribe(customers => { 17 | this.customers = customers; 18 | }); 19 | } 20 | } 21 | 22 | getCustomer(id: string) { 23 | if (this.customers) { 24 | const cached = this.customers.find(v => v.id === id); 25 | console.log('use cached'); 26 | return of(cached); 27 | } else { 28 | console.log('use db'); 29 | return this.db.collection('customers').doc(id).valueChanges(); 30 | } 31 | 32 | } 33 | 34 | dispose() { 35 | this.subscription.unsubscribe(); 36 | this.customers = null; 37 | } 38 | 39 | 40 | 41 | constructor(private db: AngularFirestore) { } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/customers/customers-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ListPageComponent } from './list-page/list-page.component'; 4 | import { DetailPageComponent } from './detail-page/detail-page.component'; 5 | 6 | 7 | const routes: Routes = [ 8 | { path: '', component: ListPageComponent }, 9 | { path: ':id', component: DetailPageComponent } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class CustomersRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/customers/customers.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { CustomersRoutingModule } from './customers-routing.module'; 5 | import { ListPageComponent } from './list-page/list-page.component'; 6 | import { DetailPageComponent } from './detail-page/detail-page.component'; 7 | import { SharedModule } from '../shared/shared.module'; 8 | 9 | 10 | @NgModule({ 11 | declarations: [ListPageComponent, DetailPageComponent], 12 | imports: [ 13 | CommonModule, 14 | SharedModule, 15 | CustomersRoutingModule 16 | ] 17 | }) 18 | export class CustomersModule { } 19 | -------------------------------------------------------------------------------- /src/app/customers/detail-page/detail-page.component.html: -------------------------------------------------------------------------------- 1 |

Detail Page

2 | 3 | 4 | 5 |

This content is server rendered with Angular Universal, NestJS, and Google Cloud Run.

6 | 7 | Paste URL:
https://firestarter.fireship.io/customers/{{customerId}}
8 | 9 | Twitter Card Validator 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | {{ cust.name }} 19 | Customer Details 20 | 21 | 22 | 23 |

24 | {{ cust.bio }} 25 |

26 |
27 |
28 | -------------------------------------------------------------------------------- /src/app/customers/detail-page/detail-page.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | text-align: center; 3 | padding: 24px; 4 | } 5 | mat-card { 6 | max-width: 450px; 7 | background-color:#212121; 8 | margin: 0 auto 10px; 9 | } -------------------------------------------------------------------------------- /src/app/customers/detail-page/detail-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { AngularFirestore } from '@angular/fire/firestore'; 4 | import { tap } from 'rxjs/operators'; 5 | import { SeoService } from 'src/app/services/seo.service'; 6 | import { CustomerDataService } from '../customer-data.service'; 7 | import { Observable } from 'rxjs'; 8 | 9 | @Component({ 10 | selector: 'app-detail-page', 11 | templateUrl: './detail-page.component.html', 12 | styleUrls: ['./detail-page.component.scss'] 13 | }) 14 | export class DetailPageComponent implements OnInit { 15 | customerId: string; 16 | customer: Observable; 17 | 18 | constructor( 19 | private route: ActivatedRoute, 20 | private db: AngularFirestore, 21 | private seo: SeoService, 22 | public data: CustomerDataService 23 | ) {} 24 | 25 | ngOnInit() { 26 | this.customerId = this.route.snapshot.paramMap.get('id'); 27 | 28 | // this.customer = this.db 29 | // .collection('customers') 30 | // .doc(customerId) 31 | // .valueChanges() 32 | this.customer = this.data.getCustomer(this.customerId) 33 | .pipe( 34 | tap(cust => 35 | this.seo.generateTags({ 36 | title: cust.name, 37 | description: cust.bio, 38 | image: cust.image, 39 | }) 40 | ) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/customers/list-page/list-page.component.html: -------------------------------------------------------------------------------- 1 |

List Page

2 | 3 | 4 |

Customers (Fake Data)

5 | 6 | 7 | account_box 8 |

{{ cust.name }}

9 |
10 |
-------------------------------------------------------------------------------- /src/app/customers/list-page/list-page.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | text-align: center; 3 | padding: 24px; 4 | } 5 | mat-nav-list { 6 | max-width: 600px; 7 | background-color:#212121; 8 | padding: 0; 9 | margin: 0 auto; 10 | } 11 | mat-list-item { 12 | 13 | transition: transform 300ms ease; 14 | &:hover { 15 | transform: translateY(-3px); 16 | } 17 | transition: transform 300ms ease; 18 | } -------------------------------------------------------------------------------- /src/app/customers/list-page/list-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { SeoService } from 'src/app/services/seo.service'; 3 | import { AngularFirestore } from '@angular/fire/firestore'; 4 | import { tap } from 'rxjs/operators'; 5 | import { CustomerDataService } from '../customer-data.service'; 6 | 7 | @Component({ 8 | selector: 'app-list-page', 9 | templateUrl: './list-page.component.html', 10 | styleUrls: ['./list-page.component.scss'] 11 | }) 12 | export class ListPageComponent implements OnInit { 13 | customers; 14 | 15 | constructor(private seo: SeoService, private db: AngularFirestore, public data: CustomerDataService) {} 16 | 17 | ngOnInit() { 18 | this.seo.generateTags({ 19 | title: 'Customer List', 20 | description: 'A list filled with customers' 21 | }); 22 | 23 | // this.customers = this.db.collection('customers').valueChanges({ idField: 'id' }); 24 | 25 | this.data.subscribeToCustomers(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Angular Firestarter

4 | 5 | 6 | 7 | 8 |

9 | Build this App 10 | Source 11 |

12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.scss: -------------------------------------------------------------------------------- 1 | header { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home-page', 5 | templateUrl: './home-page.component.html', 6 | styleUrls: ['./home-page.component.scss'] 7 | }) 8 | export class HomePageComponent {} 9 | -------------------------------------------------------------------------------- /src/app/kanban/board.model.ts: -------------------------------------------------------------------------------- 1 | export interface Board { 2 | id?: string; 3 | title?: string; 4 | priority?: number; 5 | tasks?: Task[]; 6 | } 7 | 8 | export interface Task { 9 | description?: string; 10 | label?: 'purple' | 'blue' | 'green' | 'yellow' | 'red' | 'gray'; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/kanban/board.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFireAuth } from '@angular/fire/auth'; 3 | import { AngularFirestore } from '@angular/fire/firestore'; 4 | import firebase from 'firebase/app'; 5 | import { switchMap, map } from 'rxjs/operators'; 6 | import { Board, Task } from './board.model'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class BoardService { 12 | constructor(private afAuth: AngularFireAuth, private db: AngularFirestore) {} 13 | 14 | /** 15 | * Creates a new board for the current user 16 | */ 17 | async createBoard(data: Board) { 18 | const user = await this.afAuth.currentUser; 19 | return this.db.collection('boards').add({ 20 | ...data, 21 | uid: user.uid, 22 | tasks: [{ description: 'Hello!', label: 'yellow' }] 23 | }); 24 | } 25 | 26 | /** 27 | * Get all boards owned by current user 28 | */ 29 | getUserBoards() { 30 | return this.afAuth.authState.pipe( 31 | switchMap(user => { 32 | if (user) { 33 | return this.db 34 | .collection('boards', ref => 35 | ref.where('uid', '==', user.uid).orderBy('priority') 36 | ) 37 | .valueChanges({ idField: 'id' }); 38 | } else { 39 | return []; 40 | } 41 | }), 42 | // map(boards => boards.sort((a, b) => a.priority - b.priority)) 43 | ); 44 | } 45 | 46 | /** 47 | * Run a batch write to change the priority of each board for sorting 48 | */ 49 | sortBoards(boards: Board[]) { 50 | const db = firebase.firestore(); 51 | const batch = db.batch(); 52 | const refs = boards.map(b => db.collection('boards').doc(b.id)); 53 | refs.forEach((ref, idx) => batch.update(ref, { priority: idx })); 54 | batch.commit(); 55 | } 56 | 57 | /** 58 | * Delete board 59 | */ 60 | deleteBoard(boardId: string) { 61 | return this.db 62 | .collection('boards') 63 | .doc(boardId) 64 | .delete(); 65 | } 66 | 67 | /** 68 | * Updates the tasks on board 69 | */ 70 | updateTasks(boardId: string, tasks: Task[]) { 71 | return this.db 72 | .collection('boards') 73 | .doc(boardId) 74 | .update({ tasks }); 75 | } 76 | 77 | /** 78 | * Remove a specifc task from the board 79 | */ 80 | removeTask(boardId: string, task: Task) { 81 | return this.db 82 | .collection('boards') 83 | .doc(boardId) 84 | .update({ 85 | tasks: firebase.firestore.FieldValue.arrayRemove(task) 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/kanban/board/board.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ board.title }} 7 | 8 | 9 | {{ board.id }} 10 | 11 | 12 | 13 |
19 |
25 | {{ task.description }} 26 |
27 | 28 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /src/app/kanban/board/board.component.scss: -------------------------------------------------------------------------------- 1 | .outer-card { 2 | margin: 10px; 3 | min-width: 300px; 4 | max-width: 300px; 5 | padding: 10px; 6 | background: #212121; 7 | } 8 | 9 | .inner-card { 10 | margin: 5px 0; 11 | cursor: pointer; 12 | } 13 | 14 | .cdk-drag-animating { 15 | transition: transform 300ms ease; 16 | } 17 | 18 | .tasks.cdk-drop-list-dragging .cdk-drag { 19 | transition: transform 300ms ease; 20 | } 21 | 22 | .cdk-drag-placeholder { 23 | opacity: 0.5; 24 | } 25 | 26 | .blue { background: #71deff; color: black; } 27 | .green { background: #36e9b6; color: black; } 28 | .yellow { background: #ffcf44; color: black; } 29 | .purple { background: #b15cff; } 30 | .red { background: #e74a4a; } 31 | 32 | .gray { background: gray; text-decoration: line-through; } -------------------------------------------------------------------------------- /src/app/kanban/board/board.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; 3 | import { TaskDialogComponent } from '../dialogs/task-dialog.component'; 4 | import { MatDialog } from '@angular/material/dialog'; 5 | import { BoardService } from '../board.service'; 6 | import { Task } from '../board.model'; 7 | 8 | @Component({ 9 | selector: 'app-board', 10 | templateUrl: './board.component.html', 11 | styleUrls: ['./board.component.scss'] 12 | }) 13 | export class BoardComponent { 14 | @Input() board; 15 | 16 | taskDrop(event: CdkDragDrop) { 17 | moveItemInArray(this.board.tasks, event.previousIndex, event.currentIndex); 18 | this.boardService.updateTasks(this.board.id, this.board.tasks); 19 | } 20 | 21 | openDialog(task?: Task, idx?: number): void { 22 | const newTask = { label: 'purple' }; 23 | const dialogRef = this.dialog.open(TaskDialogComponent, { 24 | width: '500px', 25 | data: task 26 | ? { task: { ...task }, isNew: false, boardId: this.board.id, idx } 27 | : { task: newTask, isNew: true } 28 | }); 29 | 30 | dialogRef.afterClosed().subscribe(result => { 31 | if (result) { 32 | if (result.isNew) { 33 | this.boardService.updateTasks(this.board.id, [ 34 | ...this.board.tasks, 35 | result.task 36 | ]); 37 | } else { 38 | const update = this.board.tasks; 39 | update.splice(result.idx, 1, result.task); 40 | this.boardService.updateTasks(this.board.id, this.board.tasks); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | handleDelete() { 47 | this.boardService.deleteBoard(this.board.id); 48 | } 49 | 50 | constructor(private boardService: BoardService, private dialog: MatDialog) {} 51 | } 52 | -------------------------------------------------------------------------------- /src/app/kanban/boards-list/boards-list.component.html: -------------------------------------------------------------------------------- 1 |
7 | 8 | drag_indicator 9 | 10 | 11 |
12 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/kanban/boards-list/boards-list.component.scss: -------------------------------------------------------------------------------- 1 | .boards { 2 | width: auto; 3 | padding: 24px; 4 | display: flex; 5 | flex-direction: row; 6 | overflow-x: scroll; 7 | &::-webkit-scrollbar { 8 | height: 4px; 9 | width: 4px 10 | } 11 | &::-webkit-scrollbar-track { 12 | // box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 13 | background-color: #000; 14 | } 15 | &::-webkit-scrollbar { 16 | width: 3px; 17 | background-color: #000; 18 | } 19 | 20 | &::-webkit-scrollbar-thumb { 21 | background-color: #f5f5f5; 22 | border: 2px solid #555555; 23 | } 24 | 25 | .handle { 26 | position: relative; 27 | top: 5px; 28 | left: 0; 29 | cursor: move; 30 | } 31 | } 32 | 33 | 34 | .cdk-drag-placeholder { 35 | opacity: 0.2; 36 | width: 350px; 37 | border: 5px dashed gray; 38 | margin: 0 10px; 39 | 40 | } 41 | 42 | .cdk-drag-animating { 43 | transition: transform 300ms ease; 44 | } 45 | 46 | .boards.cdk-drop-list-dragging .cdk-drag { 47 | transition: transform 300ms ease; 48 | } 49 | 50 | 51 | .board-button { 52 | display: flex; 53 | flex-direction: column; 54 | align-items: center; 55 | justify-content: center; 56 | border: 5px gray dashed; 57 | width: 300px; 58 | padding: 32px; 59 | height: 350px; 60 | } -------------------------------------------------------------------------------- /src/app/kanban/boards-list/boards-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; 3 | import { Subscription } from 'rxjs'; 4 | import { MatDialog } from '@angular/material/dialog'; 5 | import { BoardDialogComponent } from '../dialogs/board-dialog.component'; 6 | import { Board } from '../board.model'; 7 | import { BoardService } from '../board.service'; 8 | 9 | @Component({ 10 | selector: 'app-boards-list', 11 | templateUrl: './boards-list.component.html', 12 | styleUrls: ['./boards-list.component.scss'] 13 | }) 14 | export class BoardsListComponent implements OnInit, OnDestroy { 15 | 16 | boards: Board[]; 17 | sub: Subscription; 18 | 19 | constructor(public boardService: BoardService, public dialog: MatDialog) {} 20 | 21 | ngOnInit() { 22 | this.sub = this.boardService 23 | .getUserBoards() 24 | .subscribe(boards => (this.boards = boards)); 25 | } 26 | 27 | drop(event: CdkDragDrop) { 28 | moveItemInArray(this.boards, event.previousIndex, event.currentIndex); 29 | this.boardService.sortBoards(this.boards); 30 | } 31 | 32 | openBoardDialog(): void { 33 | const dialogRef = this.dialog.open(BoardDialogComponent, { 34 | width: '400px', 35 | data: { } 36 | }); 37 | 38 | dialogRef.afterClosed().subscribe(result => { 39 | if (result) { 40 | this.boardService.createBoard({ 41 | title: result, 42 | priority: this.boards.length 43 | }); 44 | } 45 | }); 46 | } 47 | 48 | ngOnDestroy() { 49 | this.sub.unsubscribe(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/kanban/dialogs/board-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-board-dialog', 6 | template: ` 7 |

Board

8 |
9 |

What shall we call this board?

10 | 11 | 12 | 13 |
14 |
15 | 16 | 19 |
20 | ` 21 | }) 22 | export class BoardDialogComponent { 23 | constructor( 24 | public dialogRef: MatDialogRef, 25 | @Inject(MAT_DIALOG_DATA) public data: any 26 | ) {} 27 | 28 | onNoClick(): void { 29 | this.dialogRef.close(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/kanban/dialogs/dialog.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | overflow: hidden; 3 | height: auto; 4 | padding: 20px; 5 | width: 100%; 6 | } 7 | 8 | mat-form-field { 9 | width: 100%; 10 | } 11 | 12 | textarea { display: block; width: 100%; } 13 | 14 | .blue { color: #71deff; } 15 | .green { color: #36e9b6; } 16 | .yellow { color: #ffcf44; } 17 | .purple { color: #b15cff; } 18 | .gray { color: gray; } 19 | .red { color: #e74a4a; } -------------------------------------------------------------------------------- /src/app/kanban/dialogs/task-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | import { BoardService } from '../board.service'; 4 | 5 | @Component({ 6 | selector: 'app-task-dialog', 7 | styleUrls: ['./dialog.scss'], 8 | template: ` 9 |

Task

10 |
11 | 12 | 17 | 18 |
19 | 23 | 24 | {{ 25 | opt === 'gray' ? 'check_circle' : 'lens' 26 | }} 27 | 28 | 29 |
30 |
31 | 34 | 35 | 39 |
40 | ` 41 | }) 42 | export class TaskDialogComponent { 43 | labelOptions = ['purple', 'blue', 'green', 'yellow', 'red', 'gray']; 44 | 45 | constructor( 46 | public dialogRef: MatDialogRef, 47 | private boardService: BoardService, 48 | @Inject(MAT_DIALOG_DATA) public data: any 49 | ) {} 50 | 51 | onNoClick(): void { 52 | this.dialogRef.close(); 53 | } 54 | 55 | handleTaskDelete() { 56 | this.boardService.removeTask(this.data.boardId, this.data.task); 57 | this.dialogRef.close(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/kanban/kanban-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { BoardsListComponent } from './boards-list/boards-list.component'; 4 | 5 | 6 | const routes: Routes = [ 7 | { path: '', component: BoardsListComponent } 8 | ]; 9 | 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class KanbanRoutingModule { } 16 | 17 | -------------------------------------------------------------------------------- /src/app/kanban/kanban.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { KanbanRoutingModule } from './kanban-routing.module'; 5 | import { BoardsListComponent } from './boards-list/boards-list.component'; 6 | import { DragDropModule } from '@angular/cdk/drag-drop'; 7 | import { RouterModule } from '@angular/router'; 8 | import { SharedModule } from '../shared/shared.module'; 9 | import { MatDialogModule } from '@angular/material/dialog'; 10 | import { BoardComponent } from './board/board.component'; 11 | import { FormsModule } from '@angular/forms'; 12 | import { BoardDialogComponent } from './dialogs/board-dialog.component'; 13 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 14 | import { TaskDialogComponent } from './dialogs/task-dialog.component'; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | BoardsListComponent, 19 | BoardComponent, 20 | BoardDialogComponent, 21 | TaskDialogComponent 22 | ], 23 | imports: [ 24 | CommonModule, 25 | RouterModule, 26 | SharedModule, 27 | KanbanRoutingModule, 28 | FormsModule, 29 | DragDropModule, 30 | MatDialogModule, 31 | MatButtonToggleModule, 32 | ], 33 | entryComponents: [BoardDialogComponent, TaskDialogComponent] 34 | }) 35 | export class KanbanModule {} 36 | -------------------------------------------------------------------------------- /src/app/services/seo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Title, Meta } from '@angular/platform-browser'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class SeoService { 9 | 10 | constructor(private title: Title, private meta: Meta, private router: Router) { } 11 | 12 | generateTags({ title = '', description = '', image = '' }) { 13 | 14 | this.title.setTitle(title); 15 | this.meta.addTags([ 16 | // Open Graph 17 | { name: 'og:url', content: `https://firestarter.fireship.io${this.router.url}` }, 18 | { name: 'og:title', content: title }, 19 | { name: 'og:description', content: description }, 20 | { name: 'og:image', content: image }, 21 | // Twitter Card 22 | { name: 'twitter:card', content: 'summary' }, 23 | { name: 'twitter:site', content: '@fireship_dev' }, 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/snack.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import { tap } from 'rxjs/operators'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class SnackService { 10 | constructor(private snackBar: MatSnackBar, private router: Router) {} 11 | 12 | authError() { 13 | this.snackBar.open('You must be logged in!', 'OK', { 14 | duration: 5000 15 | }); 16 | 17 | return this.snackBar._openedSnackBarRef 18 | .onAction() 19 | .pipe(tap(_ => this.router.navigate(['/login']))) 20 | .subscribe(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/delete-button/delete-button.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/shared/delete-button/delete-button.component.scss: -------------------------------------------------------------------------------- 1 | .confirm { min-width: 200px; } -------------------------------------------------------------------------------- /src/app/shared/delete-button/delete-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-delete-button', 5 | templateUrl: './delete-button.component.html', 6 | styleUrls: ['./delete-button.component.scss'] 7 | }) 8 | export class DeleteButtonComponent { 9 | canDelete: boolean; 10 | 11 | @Output() delete = new EventEmitter(); 12 | 13 | cancel() { 14 | this.canDelete = false; 15 | } 16 | 17 | prepareForDelete() { 18 | this.canDelete = true; 19 | } 20 | 21 | deleteBoard() { 22 | this.delete.emit(true); 23 | this.canDelete = false; 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatToolbarModule } from '@angular/material/toolbar'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { ShellComponent } from './shell/shell.component'; 6 | import { LayoutModule } from '@angular/cdk/layout'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatSidenavModule } from '@angular/material/sidenav'; 9 | import { MatListModule } from '@angular/material/list'; 10 | import { MatMenuModule } from '@angular/material/menu'; 11 | import { RouterModule } from '@angular/router'; 12 | import { DeleteButtonComponent } from './delete-button/delete-button.component'; 13 | import { MatCardModule } from '@angular/material/card'; 14 | import { MatFormFieldModule } from '@angular/material/form-field'; 15 | import { MatInputModule } from '@angular/material/input'; 16 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 17 | 18 | const components = [ 19 | ShellComponent, DeleteButtonComponent 20 | ]; 21 | 22 | const modules = [ 23 | CommonModule, 24 | RouterModule, 25 | MatToolbarModule, 26 | MatIconModule, 27 | LayoutModule, 28 | MatButtonModule, 29 | MatSidenavModule, 30 | MatListModule, 31 | MatMenuModule, 32 | MatIconModule, 33 | MatCardModule, 34 | MatFormFieldModule, 35 | MatInputModule, 36 | MatSnackBarModule 37 | ]; 38 | 39 | @NgModule({ 40 | declarations: [ 41 | ...components 42 | ], 43 | imports: [ 44 | ...modules, 45 | ], 46 | exports: [ 47 | ...components, 48 | ...modules 49 | ], 50 | }) 51 | export class SharedModule {} 52 | -------------------------------------------------------------------------------- /src/app/shared/shell/shell.component.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | Menu 7 | 8 | 9 | Home 10 | Login 11 | Kanban Demo 12 | SSR Demo 13 | 14 | 15 | 16 | 17 | 27 | 28 | 29 | 30 | 41 | 42 | 45 | 46 | 47 | 48 | 💬 49 | Chat on Slack 50 | 51 | 52 | 💾 53 | Code on Github 54 | 55 | 56 | 🔥 57 | Full Course on Fireship 58 | 59 | 60 | 61 | 📺 62 | Watch on the YouTube 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/app/shared/shell/shell.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .logo { 6 | font-size: 1.2em; 7 | cursor: pointer; 8 | outline: none; 9 | } 10 | 11 | .mat-drawer-side { 12 | border: none; 13 | box-shadow: 1px 1px 10px 1px rgba(0, 0, 0, 0.3); 14 | } 15 | 16 | .sidenav { 17 | width: 200px; 18 | font-family: sofia-pro; 19 | } 20 | 21 | .sidenav .mat-toolbar { 22 | background: inherit; 23 | } 24 | 25 | .mat-toolbar.mat-primary { 26 | position: sticky; 27 | top: 0; 28 | z-index: 1; 29 | } 30 | 31 | .fill-space { 32 | flex: 1 1 auto; 33 | } 34 | 35 | i { 36 | font-style: normal; 37 | font-size: 1.25em; 38 | margin-right: 16px; 39 | } 40 | 41 | .avatar { 42 | width: 1.75em; 43 | border-radius: 50%; 44 | margin: 0 10px 5px; 45 | } -------------------------------------------------------------------------------- /src/app/shared/shell/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 3 | import { Observable } from 'rxjs'; 4 | import { map, shareReplay } from 'rxjs/operators'; 5 | import { AngularFireAuth } from '@angular/fire/auth'; 6 | 7 | @Component({ 8 | selector: 'app-shell', 9 | templateUrl: './shell.component.html', 10 | styleUrls: ['./shell.component.scss'] 11 | }) 12 | export class ShellComponent { 13 | 14 | isHandset$: Observable = this.breakpointObserver.observe([Breakpoints.Handset]) 15 | .pipe( 16 | map(result => result.matches), 17 | shareReplay() 18 | ); 19 | 20 | constructor(private breakpointObserver: BreakpointObserver, public afAuth: AngularFireAuth) {} 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/user/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | CanActivate, 4 | ActivatedRouteSnapshot, 5 | RouterStateSnapshot, 6 | } from '@angular/router'; 7 | import { AngularFireAuth } from '@angular/fire/auth'; 8 | import { SnackService } from '../services/snack.service'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AuthGuard implements CanActivate { 14 | constructor( 15 | private afAuth: AngularFireAuth, 16 | private snack: SnackService, 17 | ) {} 18 | async canActivate( 19 | next: ActivatedRouteSnapshot, 20 | state: RouterStateSnapshot 21 | ): Promise { 22 | const user = await this.afAuth.currentUser; 23 | const isLoggedIn = !!user; 24 | if (!isLoggedIn) { 25 | this.snack.authError(); 26 | } 27 | return isLoggedIn; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/app/user/email-login/email-login.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Create Account

4 | 5 | 8 |
9 | 10 |
11 |

Sign In

12 | 15 |
16 | 17 |
18 |

Reset Password

19 | 20 |
21 | 22 |
23 | 24 | 31 | 32 | 33 | You must enter a valid email address 34 | 35 | 36 | 37 | 38 | 45 | 46 | 47 | Password must be at least 6 characters long 48 | 49 | 50 | 51 | 55 | 62 | 63 | 64 | Password does not match 65 | 66 | 67 | 68 | {{ serverMessage }} 69 | 70 | 78 | 79 | 88 |
89 | 90 | 97 |
98 | -------------------------------------------------------------------------------- /src/app/user/email-login/email-login.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | min-width: 150px; 3 | max-width: 500px; 4 | width: 100%; 5 | margin: 0 auto; 6 | } 7 | 8 | mat-form-field { 9 | width: 100%; 10 | margin-bottom: 16px; 11 | } 12 | 13 | .server-error { 14 | margin: 8px 0;; 15 | } 16 | 17 | 18 | input { 19 | height: 2em; 20 | div.mat-form-field-infix { 21 | padding: .3em 0; 22 | input.mat-input-element { 23 | vertical-align: top; 24 | } 25 | } 26 | } 27 | div.mat-form-field-wrapper{ 28 | padding-bottom: 1.15em; 29 | } -------------------------------------------------------------------------------- /src/app/user/email-login/email-login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { 3 | FormBuilder, 4 | FormGroup, 5 | Validators, 6 | } from '@angular/forms'; 7 | import { AngularFireAuth } from '@angular/fire/auth'; 8 | 9 | @Component({ 10 | selector: 'app-email-login', 11 | templateUrl: './email-login.component.html', 12 | styleUrls: ['./email-login.component.scss'] 13 | }) 14 | export class EmailLoginComponent implements OnInit { 15 | form: FormGroup; 16 | 17 | type: 'login' | 'signup' | 'reset' = 'signup'; 18 | loading = false; 19 | 20 | serverMessage: string; 21 | 22 | constructor(private afAuth: AngularFireAuth, private fb: FormBuilder) {} 23 | 24 | ngOnInit() { 25 | this.form = this.fb.group({ 26 | email: ['', [Validators.required, Validators.email]], 27 | password: [ 28 | '', 29 | [Validators.minLength(6), Validators.required] 30 | ], 31 | passwordConfirm: ['', []] 32 | }); 33 | } 34 | 35 | changeType(val) { 36 | this.type = val; 37 | } 38 | 39 | get isLogin() { 40 | return this.type === 'login'; 41 | } 42 | 43 | get isSignup() { 44 | return this.type === 'signup'; 45 | } 46 | 47 | get isPasswordReset() { 48 | return this.type === 'reset'; 49 | } 50 | 51 | get email() { 52 | return this.form.get('email'); 53 | } 54 | get password() { 55 | return this.form.get('password'); 56 | } 57 | 58 | get passwordConfirm() { 59 | return this.form.get('passwordConfirm'); 60 | } 61 | 62 | get passwordDoesMatch() { 63 | if (this.type !== 'signup') { 64 | return true; 65 | } else { 66 | return this.password.value === this.passwordConfirm.value; 67 | } 68 | } 69 | 70 | async onSubmit() { 71 | this.loading = true; 72 | 73 | const email = this.email.value; 74 | const password = this.password.value; 75 | 76 | try { 77 | if (this.isLogin) { 78 | await this.afAuth.signInWithEmailAndPassword(email, password); 79 | } 80 | if (this.isSignup) { 81 | await this.afAuth.createUserWithEmailAndPassword(email, password); 82 | } 83 | if (this.isPasswordReset) { 84 | await this.afAuth.sendPasswordResetEmail(email); 85 | this.serverMessage = 'Check your email'; 86 | } 87 | } catch (err) { 88 | this.serverMessage = err; 89 | } 90 | 91 | this.loading = false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/user/google-signin.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener } from '@angular/core'; 2 | import { AngularFireAuth } from '@angular/fire/auth'; 3 | import firebase from 'firebase/app'; 4 | 5 | @Directive({ 6 | selector: '[appGoogleSignin]' 7 | }) 8 | export class GoogleSigninDirective { 9 | constructor(private afAuth: AngularFireAuth) {} 10 | 11 | @HostListener('click') 12 | onclick() { 13 | this.afAuth.signInWithPopup(new firebase.auth.GoogleAuthProvider()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/user/login-page/login-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 | 5 | 8 | 9 |
OR
10 | 11 | 12 |
13 | 14 |
15 |

16 | Logged in as {{ user.email }} 17 |

18 | 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/user/login-page/login-page.component.scss: -------------------------------------------------------------------------------- 1 | button { 2 | img { 3 | width: 1.25em; 4 | } 5 | } 6 | 7 | 8 | :host { 9 | text-align: center; 10 | } 11 | 12 | .logout { 13 | padding-top: 5em; 14 | } -------------------------------------------------------------------------------- /src/app/user/login-page/login-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AngularFireAuth } from '@angular/fire/auth'; 3 | 4 | @Component({ 5 | selector: 'app-login-page', 6 | templateUrl: './login-page.component.html', 7 | styleUrls: ['./login-page.component.scss'] 8 | }) 9 | export class LoginPageComponent { 10 | constructor(public afAuth: AngularFireAuth) { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/user/user-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LoginPageComponent } from '../user/login-page/login-page.component'; 4 | 5 | 6 | const routes: Routes = [ 7 | { path: '', component: LoginPageComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class UserRoutingModule { } 15 | -------------------------------------------------------------------------------- /src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { UserRoutingModule } from './user-routing.module'; 5 | import { GoogleSigninDirective } from './google-signin.directive'; 6 | import { EmailLoginComponent } from './email-login/email-login.component'; 7 | import { LoginPageComponent } from './login-page/login-page.component'; 8 | import { SharedModule } from '../shared/shared.module'; 9 | import { ReactiveFormsModule } from '@angular/forms'; 10 | 11 | 12 | @NgModule({ 13 | declarations: [GoogleSigninDirective, EmailLoginComponent, LoginPageComponent], 14 | exports: [GoogleSigninDirective], 15 | imports: [ 16 | CommonModule, 17 | SharedModule, 18 | UserRoutingModule, 19 | ReactiveFormsModule, 20 | ] 21 | }) 22 | export class UserModule { } 23 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/default-user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/google-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/google-signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/google-signin.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | firebase: { 4 | apiKey: 'AIzaSyAKlEX5a8PQaadYNSrohqfl33O37Kjkv1Y', 5 | authDomain: 'angular-voxer.firebaseapp.com', 6 | databaseURL: 'https://angular-voxer.firebaseio.com', 7 | projectId: 'angular-voxer', 8 | storageBucket: 'angular-voxer.appspot.com', 9 | messagingSenderId: '122050526979', 10 | appId: '1:122050526979:web:529304dce5ffca7a' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | firebase: { 8 | apiKey: 'AIzaSyAKlEX5a8PQaadYNSrohqfl33O37Kjkv1Y', 9 | authDomain: 'angular-voxer.firebaseapp.com', 10 | databaseURL: 'https://angular-voxer.firebaseio.com', 11 | projectId: 'angular-voxer', 12 | storageBucket: 'angular-voxer.appspot.com', 13 | messagingSenderId: '122050526979', 14 | appId: '1:122050526979:web:529304dce5ffca7a' 15 | } 16 | }; 17 | 18 | /* 19 | * For easier debugging in development mode, you can import the following file 20 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 21 | * 22 | * This import should be commented out in production mode because it will have a negative impact 23 | * on performance if an error is thrown. 24 | */ 25 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 26 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/angular-firestarter/df4863378dc4060b785a25fa0eb86fa1df813801/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Firestarter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | import { environment } from './environments/environment'; 4 | 5 | if (environment.production) { 6 | enableProdMode(); 7 | } 8 | 9 | export { AppServerModule } from './app/app.server.module'; 10 | export { ngExpressEngine } from "@nguniversal/express-engine"; 11 | 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | document.addEventListener('DOMContentLoaded', () => { 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.error(err)); 15 | }); 16 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firestarter", 3 | "short_name": "firestarter", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // THIS --> https://material.io/design/color/#tools-for-picking-colors 5 | // https://material.io/resources/color/#!/?view.left=0&view.right=0&primary.color=fa8242 6 | 7 | // Custom Theming for Angular Material 8 | // For more information: https://material.angular.io/guide/theming 9 | @import '~@angular/material/theming'; 10 | @import url("https://use.typekit.net/jnq3uxd.css"); 11 | // Plus imports for other components in your app. 12 | 13 | // Include the common styles for Angular Material. We include this here so that you only 14 | // have to load a single css file for Angular Material in your app. 15 | // Be sure that you only ever include this mixin once! 16 | $custom-typography: mat-typography-config( 17 | $font-family: 'sofia-pro' 18 | ); 19 | 20 | 21 | @include mat-core($custom-typography); 22 | 23 | $custom-orange: ( 24 | 50: #fff3e0, 25 | 100: #ffe0b2, 26 | 200: #ffcc80, 27 | 300: #ffb74d, 28 | 400: #ffa726, 29 | 500: #ff9800, 30 | 600: #fb8c00, 31 | 700: #f57c00, 32 | 800: #ef6c00, 33 | 900: #e65100, 34 | A100: #ffd180, 35 | A200: #ffab40, 36 | A400: #ff9100, 37 | A700: #ff6d00, 38 | contrast: ( 39 | 50: $black-87-opacity, 40 | 100: $black-87-opacity, 41 | 200: $black-87-opacity, 42 | 300: $black-87-opacity, 43 | 400: $black-87-opacity, 44 | 500: white, 45 | 600: white, 46 | 700: white, 47 | 800: white, 48 | 900: white, 49 | A100: $black-87-opacity, 50 | A200: $black-87-opacity, 51 | A400: $black-87-opacity, 52 | A700: white, 53 | ) 54 | ); 55 | 56 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 57 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 58 | // hue. Available color palettes: https://material.io/design/color/ 59 | $firestarter-primary: mat-palette($custom-orange); 60 | $firestarter-accent: mat-palette($mat-green, A200, A100, A400); 61 | 62 | // The warn palette is optional (defaults to red). 63 | $firestarter-warn: mat-palette($mat-red); 64 | 65 | // Create the theme object (a Sass map containing all of the palettes). 66 | $firestarter-theme: mat-dark-theme($firestarter-primary, $firestarter-accent, $firestarter-warn); 67 | 68 | // Include theme styles for core and each component used in your app. 69 | // Alternatively, you can import and @include the theme mixins for each component 70 | // that you are using. 71 | @include angular-material-theme($firestarter-theme); 72 | 73 | /* You can add global styles to this file, and also import other style files */ 74 | 75 | 76 | 77 | html, body { height: 100%; } 78 | body { margin: 0; font-family: sofia-pro, sans-serif; } 79 | a { 80 | color: white; 81 | text-decoration: none; 82 | } -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app-server", 5 | "module": "commonjs" 6 | }, 7 | "files": [ 8 | "src/main.server.ts", 9 | "server.ts" 10 | ], 11 | "angularCompilerOptions": { 12 | "entryModule": "./src/app/app.server.module#AppServerModule" 13 | } , 14 | "include": [ 15 | "src/**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-use-before-declare": true, 64 | "no-var-requires": false, 65 | "object-literal-key-quotes": [ 66 | true, 67 | "as-needed" 68 | ], 69 | "object-literal-sort-keys": false, 70 | "ordered-imports": false, 71 | "quotemark": [ 72 | true, 73 | "single" 74 | ], 75 | "trailing-comma": false, 76 | "no-conflicting-lifecycle": true, 77 | "no-host-metadata-property": true, 78 | "no-input-rename": true, 79 | "no-inputs-metadata-property": true, 80 | "no-output-native": true, 81 | "no-output-on-prefix": true, 82 | "no-output-rename": true, 83 | "no-outputs-metadata-property": true, 84 | "template-banana-in-box": true, 85 | "template-no-negated-async": true, 86 | "use-lifecycle-interface": true, 87 | "use-pipe-transform-interface": true 88 | }, 89 | "rulesDirectory": [ 90 | "codelyzer" 91 | ] 92 | } -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const WebpackConfigFactory = require('@nestjs/ng-universal') 3 | .WebpackConfigFactory; 4 | 5 | module.exports = WebpackConfigFactory.create(webpack, { 6 | // Nest server for SSR 7 | server: './server/main.ts' 8 | }); 9 | --------------------------------------------------------------------------------