├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── DEBUGGING.md ├── Dockerfile ├── README.md ├── Scrapbook-1.mongo ├── angular.json ├── browserslist ├── docker-compose.debug.mongodb.yml ├── docker-compose.debug.yml ├── docker-compose.yml ├── e2e ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── meta ├── create-vikings-cosmos-mongo.js ├── create-vikings-cosmos-sdk.js ├── create-vikings-mongo.js ├── exit.js ├── heroes.js └── villains.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── proxy.conf.json ├── server.js ├── server ├── index.js ├── routes │ ├── hero.routes.js │ ├── index.js │ ├── settings.routes.js │ └── villain.routes.js └── services │ ├── cosmos-sdk │ ├── config.js │ ├── db.js │ ├── hero.service.js │ ├── index.js │ ├── settings.service.js │ └── villain.service.js │ ├── index.js │ └── mongo │ ├── config.js │ ├── db.js │ ├── hero.model.js │ ├── hero.service.js │ ├── index.js │ ├── settings.model.js │ ├── settings.service.js │ ├── villain.model.js │ └── villain.service.js ├── src ├── app │ ├── app-dev.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── animations.ts │ │ ├── core.module.ts │ │ ├── in-memory-data.service.ts │ │ ├── index.ts │ │ ├── interceptors │ │ │ ├── auth.interceptor.ts │ │ │ ├── auth.service.ts │ │ │ ├── csrf.interceptor.ts │ │ │ ├── ensure-ssl.interceptor.ts │ │ │ ├── index.ts │ │ │ ├── log-headers.interceptor.ts │ │ │ ├── log-response.interceptor.ts │ │ │ └── transform-response.interceptor.ts │ │ ├── modal │ │ │ ├── modal.component.html │ │ │ └── modal.component.ts │ │ ├── model │ │ │ ├── hero.ts │ │ │ ├── index.ts │ │ │ └── villain.ts │ │ ├── module-import-check.ts │ │ ├── settings.service.ts │ │ ├── toast.service.ts │ │ └── toolbar │ │ │ ├── toolbar.component.html │ │ │ ├── toolbar.component.scss │ │ │ └── toolbar.component.ts │ ├── heroes │ │ ├── hero-detail │ │ │ ├── hero-detail.component.html │ │ │ ├── hero-detail.component.scss │ │ │ └── hero-detail.component.ts │ │ ├── hero-list │ │ │ ├── hero-list.component.html │ │ │ ├── hero-list.component.scss │ │ │ └── hero-list.component.ts │ │ ├── hero.service.ts │ │ ├── heroes-routing.module.ts │ │ ├── heroes.module.ts │ │ └── heroes │ │ │ ├── heroes.component.html │ │ │ ├── heroes.component.scss │ │ │ └── heroes.component.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ └── home.component.ts │ ├── ng-material │ │ └── ng-material.module.ts │ ├── routes.ts │ ├── shared │ │ └── shared.module.ts │ └── villains │ │ ├── villain-detail │ │ ├── villain-detail.component.html │ │ ├── villain-detail.component.scss │ │ └── villain-detail.component.ts │ │ ├── villain-list │ │ ├── villain-list.component.html │ │ ├── villain-list.component.scss │ │ └── villain-list.component.ts │ │ ├── villain.service.ts │ │ ├── villains-routing.module.ts │ │ ├── villains.module.ts │ │ └── villains │ │ ├── villains.component.html │ │ ├── villains.component.scss │ │ └── villains.component.ts ├── assets │ ├── .gitkeep │ ├── azureappservice.png │ ├── ignite-cloud.jpg │ ├── ignite-local.jpg │ ├── jp-48.jpg │ ├── jp.jpg │ ├── ng.png │ ├── ngvikings-logo.png │ ├── wb-48.jpg │ └── wb.jpg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles │ ├── mixin.scss │ ├── styles.scss │ └── theme.scss ├── test.ts └── typings.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | README.md 9 | LICENSE 10 | .vscode -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | 3 | PORT="8626" 4 | WWW="./" 5 | 6 | # Toggle which one of these you want to use 7 | #DATA_OPTION="local_mongo" 8 | #DATA_OPTION="cloud_cosmos" 9 | DATA_OPTION="cloud_cosmos_sdk" 10 | 11 | CORE_API_KEY="your-core-api-key-goes-here" 12 | CORE_API_URL="https://vikings-core.documents.azure.com:443/" 13 | 14 | USE_LIVE_DATA="yes" 15 | MONGO_API_ACCOUNT="vikings" 16 | MONGO_API_DB="vikings-db" 17 | MONGO_API_KEY="your-mongo-api-key-goes-here" 18 | MONGO_API_PORT="10255" 19 | 20 | # use localhost or your linked docker container 21 | LOCAL_MONGO="localhost" 22 | 23 | FOO=1234 24 | BAR={1,2,3,4]} 25 | BAZ='single quoted' 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | .env 4 | .env.bak 5 | 6 | # data files 7 | /data 8 | 9 | # compiled output 10 | /dist 11 | /dist-server 12 | /tmp 13 | /out-tsc 14 | 15 | # dependencies 16 | /node_modules 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | testem.log 41 | /typings 42 | 43 | # e2e 44 | /e2e/*.js 45 | /e2e/*.map 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome", 11 | 12 | "url": "http://localhost:8080", 13 | "webRoot": "${workspaceFolder}" 14 | }, 15 | { 16 | "name": "Launch Angular", 17 | "type": "chrome", 18 | "request": "launch", 19 | "preLaunchTask": "npm: start", 20 | "url": "http://localhost:4200/", 21 | "webRoot": "${workspaceFolder}" 22 | }, 23 | { 24 | // Debug the node server directly 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Launch server.js", 28 | "program": "${workspaceFolder}/server.js", 29 | "envFile": "${workspaceFolder}/.env", 30 | "env": { 31 | "WWW": "./dist" 32 | }, 33 | "args": ["-r dotenv/config", "--inspect"] 34 | }, 35 | { 36 | // Debug the node server via npm scripts 37 | "type": "node", 38 | "request": "launch", 39 | "name": "Launch via NPM", 40 | "runtimeExecutable": "npm", 41 | "runtimeArgs": ["run-script", "local-proxy-debug"], 42 | "port": 9229 43 | }, 44 | { 45 | // Debug the attached docker node server 46 | "type": "node", 47 | "request": "attach", 48 | "name": "Docker: Attach to Node", 49 | "port": 9229, 50 | "address": "localhost", 51 | "localRoot": "${workspaceFolder}", 52 | "remoteRoot": "/usr/src/app", 53 | "protocol": "inspector" 54 | }, 55 | { 56 | // Debug whichever file is open and focused 57 | "type": "node", 58 | "request": "launch", 59 | "cwd": "${workspaceFolder}", 60 | "name": "Launch Relative File", 61 | "args": ["${relativeFile}"] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "docker.defaultRegistryPath": "papacr.azurecr.io/johnpapa", 3 | "workbench.colorCustomizations": { 4 | "activityBar.background": "#164a8e", 5 | "activityBar.activeBackground": "#164a8e", 6 | "activityBar.activeBorder": "#e13e85", 7 | "activityBar.foreground": "#e7e7e7", 8 | "activityBar.inactiveForeground": "#e7e7e799", 9 | "activityBarBadge.background": "#e13e85", 10 | "activityBarBadge.foreground": "#e7e7e7", 11 | "titleBar.activeBackground": "#0f3362", 12 | "titleBar.inactiveBackground": "#0f336299", 13 | "titleBar.activeForeground": "#e7e7e7", 14 | "titleBar.inactiveForeground": "#e7e7e799", 15 | "statusBar.background": "#0f3362", 16 | "statusBarItem.hoverBackground": "#164a8e", 17 | "statusBar.foreground": "#e7e7e7", 18 | "panel.border": "#164a8e", 19 | "sideBar.border": "#164a8e", 20 | "editorGroup.border": "#164a8e" 21 | }, 22 | "peacock.color": "#0f3362", 23 | } 24 | -------------------------------------------------------------------------------- /DEBUGGING.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | ## Auto - Attach Debugging 4 | 5 | The debugger will automatically kick in and attach. 6 | 7 | Set the`auto-attach` in the settings, then run node server with the inpsector protocol, and run ng serve 8 | 9 | 1 - time setup for auto - attach: 10 | 11 | 1. `CMD+,` 12 | 2. type`auto attach` 13 | 3. set`auto-attach` to`on` 14 | 15 | Now just run your scripts 16 | 17 | ```bash 18 | npm run local-proxy-debug 19 | ``` 20 | 21 | ## npm Debugging 22 | 23 | Launch your app and the debugger 24 | 25 | 1. Go to the debugger panel 26 | 2. Run`Launch via NPM` 27 | 28 | ## Docker Debugging 29 | 30 | Attach the debugger to a running docker container 31 | 32 | 1. Go to the debugger panel 33 | 2. `Docker: Attach to Node` 34 | 35 | ## Relative File Debugging 36 | 37 | Debug whichever file you have open and in focus. 38 | 39 | 1. open`meta/create-vikings-cosmos-sdk.js` 40 | 2. Go to the debugger panel 41 | 3. `Launch Relative File` 42 | 43 | ## Debugging Angular in VS Code 44 | 45 | 1. Run the app, without debugging node 46 | `npm run local-proxy` 47 | 2. Go to the debugger panel 48 | 3. `Launch Angular in Chrome` 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Client App 2 | FROM node:10.15-alpine as client-app 3 | LABEL authors="John Papa" 4 | WORKDIR /usr/src/app 5 | COPY ["package.json", "npm-shrinkwrap.json*", "./"] 6 | RUN npm install --silent 7 | COPY . . 8 | RUN npx ng build --prod 9 | 10 | # Node server 11 | FROM node:10.15-alpine as node-server 12 | WORKDIR /usr/src/app 13 | COPY ["package.json", "npm-shrinkwrap.json*", "./"] 14 | RUN npm install --production --silent && mv node_modules ../ 15 | COPY server.js . 16 | COPY /server /usr/src/app/server 17 | 18 | # Final image 19 | FROM node:10.15-alpine 20 | WORKDIR /usr/src/app 21 | COPY --from=node-server /usr/src /usr/src 22 | COPY --from=client-app /usr/src/app/dist ./ 23 | EXPOSE 8626 24 | # CMD ["node", "server.js"] 25 | CMD ["npm", "start"] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vikings 2 | 3 | Demo App for Vikings - 2018 4 | 5 | by [John Papa](http://twitter.com/john_papa) 6 | 7 | Angular, Node, Docker, Azure, Cosmos DB/Mongo DB! 8 | 9 | Try using 1 of these variations of databases and Node SDKs: 10 | 11 | 1. Nodejs -> Express -> Mongoose -> Mongo DB 12 | 1. Nodejs -> Express -> Mongoose -> Cosmos DB (using Mongo API) 13 | 1. Nodejs -> Express -> Cosmos SQL SDK -> Cosmos DB (using SQL API) 14 | 15 | You can also try them in Docker containers or running the Node.js app locally. 16 | 17 | ## Links 18 | 19 | Here are all of the slides/code from my presentation at [ngVikings event](https://twitter.com/ngvikingsconf) this week! 20 | 21 | - Code: 22 | - Angular extensions: 23 | - Node extensions: 24 | - Free Azure Trial: 25 | 26 | ## Requirements 27 | 28 | - Install the Angular CLI 29 | 30 | Only required when running local, _without docker_. 31 | 32 | ```bash 33 | npm install -g @angular/cli 34 | ``` 35 | 36 | ## Getting Started 37 | 38 | 1. Clone this repository 39 | 40 | ```bash 41 | git clone https://github.com/johnpapa/vikings.git 42 | cd vikings 43 | ``` 44 | 45 | 1. Install the npm packages 46 | 47 | ```bash 48 | npm install 49 | ``` 50 | 51 | 1. Configure environment settings 52 | 53 | Create a file with the following name and location `.env` and copy the contents from 54 | `.env.example` into it. Replace the values with your specific configuration. Don't worry, this 55 | file is in the `.gitignore` so it won't get pushed to github. 56 | 57 | Take care not to include extra spaces or quotes. These values are taken verbatum. 58 | 59 | ```javascript 60 | NODE_ENV=development 61 | PORT=8626 62 | WWW=./ 63 | 64 | # Toggle which one of these you want to use 65 | #DATA_OPTION=local_mongo 66 | #DATA_OPTION=cloud_cosmos 67 | DATA_OPTION=cloud_cosmos_sdk 68 | 69 | CORE_API_KEY=your-core-api-key-goes-here 70 | CORE_API_URL=https://vikings-core.documents.azure.com/?WT.mc_id=vikings-github-jopapa 71 | 72 | USE_LIVE_DATA=yes 73 | MONGO_API_ACCOUNT=vikings 74 | MONGO_API_DB=vikings-db 75 | MONGO_API_KEY=your-mongo-api-key-goes-here 76 | MONGO_API_PORT=10255 77 | 78 | # use localhost or your linked docker container (e.g. mongocontainer) 79 | LOCAL_MONGO=localhost 80 | ``` 81 | 82 | ## Running the app locally, with live refresh of the client 83 | 84 | > This option requires Mongo running locally or in a container via localhost 85 | 86 | 1. Set the appropriate `.env` file settings 87 | 88 | ```bash 89 | # e.g. for mongo accessible on localhost 90 | DATA_OPTION=local_mongo 91 | LOCAL_MONGO=localhost 92 | ``` 93 | 94 | 1. Build the Angular app and launch the node server 95 | 96 | ```bash 97 | ng build 98 | npm dev-proxy 99 | ``` 100 | 101 | 1. Open the browser to 102 | 103 | ## Running the app locally serving the client app 104 | 105 | 1. Set the appropriate `.env` file settings 106 | 107 | > This option requires Mongo running locally or in a container via localhost 108 | 109 | ```bash 110 | # e.g. for mongo accessible on localhost 111 | DATA_OPTION=local_mongo 112 | LOCAL_MONGO=localhost 113 | ``` 114 | 115 | 1. Build the Angular app and launch the node server 116 | 117 | ```bash 118 | ng build 119 | npm run local 120 | ``` 121 | 122 | 1. Open the browser to 123 | 124 | ## Docker 125 | 126 | - Install and run [Docker](https://www.docker.com/community-edition) 127 | 128 | There are various flavors of the app we can build. See the appropriate sections below. 129 | 130 | ### Docker Compose with Mongo DB 131 | 132 | This contains: 133 | 134 | - Mongoose SDK 135 | - Mongo DB in a container with mounted data volume 136 | 137 | > This image expects environment variables to be set to point to the database provider (e.g. Mongo DB or Cosmos DB) 138 | 139 | ```bash 140 | # e.g. Mongo DB in a container 141 | DATA_OPTION=local_mongo 142 | USE_LIVE_DATA=yes 143 | MONGO_API_DB=vikings-db 144 | # use localhost or your linked docker container (mongocontainer) 145 | LOCAL_MONGO=mongocontainer 146 | ``` 147 | 148 | 1. CMD+SHIFT+P `docker: compose up` 149 | 1. Select `docker-compose.debug.mongodb.yml` 150 | 1. Browse to 151 | 152 | ### Docker Compose with Cosmos DB (Mongo API) 153 | 154 | This contains: 155 | 156 | - Mongoose SDK 157 | - Cosmos DB (Mongo API) 158 | 159 | > This image expects environment variables to be set to point to the database provider (e.g. Mongo DB or Cosmos DB) 160 | 161 | ```bash 162 | # e.g. Mongo DB in a container 163 | DATA_OPTION=cloud_cosmos 164 | USE_LIVE_DATA=yes 165 | MONGO_API_ACCOUNT=vikings 166 | MONGO_API_DB=vikings-db 167 | MONGO_API_KEY=your-key 168 | MONGO_API_PORT=10255 169 | ``` 170 | 171 | 1. CMD+SHIFT+P `docker: compose up` 172 | 1. Select `docker-compose.debug.yml` 173 | 1. Browse to 174 | 175 | ### Docker Compose with Cosmos DB (SQL API) 176 | 177 | This contains: 178 | 179 | - Cosmos SQL SDK 180 | - Cosmos DB (SQL API) 181 | 182 | > This image expects environment variables to be set to point to the database provider (e.g. Mongo DB or Cosmos DB) 183 | 184 | ```bash 185 | # e.g. Mongo DB in a container 186 | DATA_OPTION=cloud_cosmos_sdk 187 | CORE_API_KEY=your-key 188 | CORE_API_URL=https://vikings-core.documents.azure.com/?WT.mc_id=vikings-github-jopapa 189 | ``` 190 | 191 | 1. CMD+SHIFT+P `docker: compose up` 192 | 1. Select `docker-compose.debug.yml` 193 | 1. Browse to 194 | 195 | ## Deploy to Azure 196 | 197 | ### Deploy to Azure Requirements 198 | 199 | 1. Azure account 200 | 201 | Free Azure Trial - 202 | 203 | 1. Install the Azure CLI 204 | 205 | 206 | 207 | 1. Azure/Node/Docker extensions for VS Code 208 | 209 | 210 | 211 | ### Deploy Docker Image to Azure 212 | 213 | 1. CMD+SHIFT+P `docker: compose up` 214 | 1. Select `docker-compose.yml` 215 | 1. Go to the Docker extension in the sidebar and expand `Images` 216 | 1. Right click the image and select `tag` 217 | 1. Prefix the tag with your container registry name 218 | e.g Azure container registry `papacr.azurecr.io/johnpapa/vikings:latest` 219 | 1. Right click the image and select `push` 220 | 1. Expand `Registries / Azure` in the Docker extension in the sidebar 221 | 1. Right click the image you pushed and select `deploy to azure app service` 222 | 1. Follow the instructions when prompted to choose your server 223 | 224 | ### Notes 225 | 226 | 1. Azure Container Registry Login command may be needed 227 | 228 | ```bash 229 | az acr login --name your-azure-container-registry-name 230 | 231 | # e.g. az acr login --name papacr 232 | ``` 233 | 234 | > If you get errors with `az acr login` try running `az account clear`. Then run `az login` again and follow the prompts. Then try again with `az acr login` 235 | 236 | ## Cosmos DB - Mongo API 237 | 238 | 1. Create a [CosmosDB instance](https://aka.ms/jp-cosmos-node) 239 | 240 | 1. Configure Cosmos DB server settings 241 | 242 | In the `.env`, set the `DATA_OPTION` to the appropraite database kind. Then adjust the `MONGO_API` settings shown below, but for your specific configuration. 243 | 244 | ```javascript 245 | DATA_OPTION = cloud_cosmos; 246 | USE_LIVE_DATA = yes; 247 | MONGO_API_ACCOUNT = your_cosmos_account; 248 | MONGO_API_DB = your_cosmos_db_name; 249 | MONGO_API_KEY = your_cosmos_db_key; 250 | MONGO_API_PORT = 10255; 251 | ``` 252 | 253 | ## Cosmos DB - SQL API 254 | 255 | 1. Create a [CosmosDB instance](https://aka.ms/jp-cosmos-node) 256 | 257 | 1. Configure Cosmos DB server settings 258 | 259 | In the `.env`, set the `DATA_OPTION` to the appropraite database kind. Then adjust the `CORE_API` settings shown below, but for your specific configuration. 260 | 261 | ```javascript 262 | DATA_OPTION = cloud_cosmos_sdk; 263 | USE_LIVE_DATA = yes; 264 | CORE_API_KEY = your_cosmos_db_key; 265 | CORE_API_URL = your_cosmos_db_url; 266 | ``` 267 | 268 | > e.g. The url should follow the format 269 | 270 | ## Resetting Your Database 271 | 272 | There are scripts in the `/meta` folder that will help you clean and repopulate the various databases uing different APIs. 273 | 274 | ## Debugging 275 | 276 | Learn various ways to debug the node and angular app in the [DEBUGGING guide](./DEBUGGING.md). 277 | 278 | ## Problems or Suggestions 279 | 280 | [Open an issue here](https://github.com/johnpapa/vikings/issues) 281 | 282 | Create the Docker image that you can `docker push` to a registry. This command uses `docker-compose` to build the image and run the container. 283 | 284 | -------------------------------------------------------------------------------- /Scrapbook-1.mongo: -------------------------------------------------------------------------------- 1 | /** 2 | Tips for executing commands 3 | 4 | Execute one line 5 | CMD "" 6 | 7 | Excute all 8 | CMD : 9 | 10 | */ 11 | 12 | db.heroes.count() 13 | 14 | db.heroes.find() 15 | 16 | db.heroes.findOne({name: 'Bjorn Ironside'}) 17 | 18 | // db.heroes.findOneAndUpdate( 19 | // { id: 5 }, 20 | // { $set: {id: 5, name: 'Aslaug', description: 'Warrior queen'}} }, 21 | // { upsert: true, new: true } 22 | // ) 23 | 24 | 25 | // db.heroes.findOneAndDelete({id: 7}) 26 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "vikings": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "inlineTemplate": true, 11 | "inlineStyle": true, 12 | "style": "scss" 13 | } 14 | }, 15 | "root": "", 16 | "sourceRoot": "src", 17 | "prefix": "vk", 18 | "architect": { 19 | "build": { 20 | "builder": "@angular-devkit/build-angular:browser", 21 | "options": { 22 | "outputPath": "dist/vikings", 23 | "index": "src/index.html", 24 | "main": "src/main.ts", 25 | "polyfills": "src/polyfills.ts", 26 | "tsConfig": "tsconfig.app.json", 27 | "aot": true, 28 | "assets": ["src/favicon.ico", "src/assets"], 29 | "stylePreprocessorOptions": { "includePaths": ["src/styles"] }, 30 | "styles": [ 31 | "src/styles/theme.scss", 32 | "src/styles/mixin.scss", 33 | "src/styles/styles.scss" 34 | ], 35 | "scripts": [] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "extractCss": true, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "6kb", 62 | "maximumError": "10kb" 63 | } 64 | ] 65 | } 66 | } 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "options": { 71 | "browserTarget": "vikings:build" 72 | }, 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "vikings:build:production" 76 | } 77 | } 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "vikings:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "tsconfig.spec.json", 91 | "karmaConfig": "karma.conf.js", 92 | "assets": ["src/favicon.ico", "src/assets"], 93 | "styles": [ 94 | "src/styles/theme.scss", 95 | "src/styles/mixin.scss", 96 | "src/styles/styles.scss" 97 | ], 98 | 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-devkit/build-angular:tslint", 104 | "options": { 105 | "tsConfig": [ 106 | "tsconfig.app.json", 107 | "tsconfig.spec.json", 108 | "e2e/tsconfig.json" 109 | ], 110 | "exclude": ["**/node_modules/**"] 111 | } 112 | }, 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "vikings:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "vikings:serve:production" 122 | } 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "defaultProject": "vikings" 129 | } 130 | -------------------------------------------------------------------------------- /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'. -------------------------------------------------------------------------------- /docker-compose.debug.mongodb.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | mongocontainer: 5 | image: mongo 6 | volumes: 7 | - ./data/vikings-db:/data/db 8 | ports: 9 | - 27017:27017 10 | vikings: 11 | image: vikings 12 | build: . 13 | env_file: 14 | - .env 15 | # environment: 16 | # NODE_ENV: development 17 | restart: always 18 | links: 19 | - mongocontainer 20 | ports: 21 | - 7626:8626 22 | - 9229:9229 23 | command: npm run start_debug 24 | # command: node --inspect=0.0.0.0:9229 server.js 25 | -------------------------------------------------------------------------------- /docker-compose.debug.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | vikings: 5 | image: vikings 6 | build: . 7 | env_file: 8 | - .env 9 | # environment: 10 | # NODE_ENV: development 11 | restart: always 12 | ports: 13 | - 7626:8626 14 | - 9229:9229 15 | command: npm run start_debug 16 | # command: node --inspect=0.0.0.0:9229 server.js 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | vikings: 5 | image: vikings 6 | build: . 7 | environment: 8 | NODE_ENV: production 9 | restart: always 10 | ports: 11 | - 7626:8626 12 | command: npm run start_azure_debug 13 | -------------------------------------------------------------------------------- /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('app01 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 | -------------------------------------------------------------------------------- /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"), 20 | reports: ["html", "lcovonly"], 21 | fixWebpackSourcePaths: true 22 | }, 23 | angularCli: { 24 | environment: "dev" 25 | }, 26 | reporters: ["progress", "kjhtml"], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ["Chrome"], 32 | singleRun: false, 33 | restartOnFileChange: true 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /meta/create-vikings-cosmos-mongo.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | require('dotenv').config(); // eslint-disable-line 3 | process.env.DATA_OPTION = 'cloud_cosmos'; // this must go before importing db.js 4 | 5 | const exit = require('./exit'); 6 | const Hero = require('../server/services/mongo/hero.model'); 7 | const Villain = require('../server/services/mongo/villain.model'); 8 | const Settings = require('../server/services/mongo/settings.model'); 9 | const heroes = require('./heroes'); 10 | const villains = require('./villains'); 11 | 12 | const settings = [{ name: 'Cosmos Mongo API' }]; 13 | 14 | const captains = console; 15 | const { mongoose, connect } = require('../server/services/mongo/db'); 16 | 17 | process.env.DATA_OPTION = 'cloud_cosmos'; 18 | 19 | connect(); 20 | 21 | go().then(() => { 22 | mongoose.connection.close(() => { 23 | console.log('Mongoose default connection is disconnected due to application termination'); 24 | process.exit(0); 25 | }); 26 | exit(`Completed successfully`); 27 | }) 28 | .catch((error) => { exit(`Completed with error ${JSON.stringify(error)}`); }); 29 | 30 | async function go() { 31 | await refreshDocuments(Hero, 'heroes', heroes); 32 | await refreshDocuments(Villain, 'villains', villains); 33 | await refreshDocuments(Settings, 'settings', settings); 34 | } 35 | 36 | async function refreshDocuments(model, collection, documents) { 37 | // Delete the collections 38 | return mongoose.connection.dropCollection(collection) 39 | // create the collections 40 | .then(() => mongoose.connection.createCollection(collection)) 41 | // Insert the items 42 | .then(() => model.insertMany(documents)) 43 | // Read the items 44 | .then(() => model.find({})) 45 | .then(docs => captains.log(`reading content: `, docs)); 46 | } 47 | -------------------------------------------------------------------------------- /meta/create-vikings-cosmos-sdk.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | require('dotenv').config(); // eslint-disable-line 3 | 4 | const cosmos = require('@azure/cosmos'); 5 | 6 | const exit = require('./exit'); 7 | const { databaseDefName, endpoint, masterKey } = require('../server/services/cosmos-sdk/config'); 8 | const heroes = require('./heroes'); 9 | const villains = require('./villains'); 10 | 11 | const captains = console; 12 | const { CosmosClient } = cosmos; 13 | const client = new CosmosClient({ endpoint, auth: { masterKey } }); 14 | const { heroContainer, villainContainer, settingsContainer } = require('../server/services/cosmos-sdk/config'); 15 | 16 | process.env.DATA_OPTION = 'cloud_cosmos_sdk'; 17 | 18 | go() 19 | .then(() => { exit(`Completed successfully`); }) 20 | .catch((error) => { exit(`Completed with error ${JSON.stringify(error)}`); }); 21 | 22 | 23 | async function go() { 24 | // Delete the database 25 | console.log(heroContainer); 26 | await client.database(databaseDefName).delete(); 27 | captains.log(`deleted database`); 28 | 29 | // Create the database 30 | const { database } = await client.databases.createIfNotExists({ 31 | id: databaseDefName, 32 | }); 33 | captains.log(`created database ${database.id}`); 34 | 35 | await bulkCreate(database, heroContainer, heroes); 36 | await bulkCreate(database, villainContainer, villains); 37 | await bulkCreate(database, settingsContainer, [{ name: 'Cosmos Core/SQL API' }]); 38 | } 39 | 40 | async function bulkCreate(database, containerDef, items) { 41 | // Create the container 42 | const containerDefinition = { 43 | id: containerDef, 44 | indexingPolicy: { automatic: true }, // turn on indexes (default) 45 | // indexingPolicy: { automatic: false }, // turn of indexes 46 | }; 47 | const { container } = await database.containers.createIfNotExists( 48 | containerDefinition, 49 | ); 50 | captains.log(`created container ${container.id}`); 51 | 52 | // Insert the items 53 | /* eslint-disable */ 54 | for (const item of items) { 55 | const { body } = await container.items.create(item); 56 | captains.log(`created item with content: `, body); 57 | } 58 | /* eslint-enable */ 59 | 60 | // Read the items 61 | const { result } = await container.items.readAll().toArray(); 62 | captains.log(result); 63 | } 64 | -------------------------------------------------------------------------------- /meta/create-vikings-mongo.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | require('dotenv').config(); // eslint-disable-line 3 | 4 | process.env.DATA_OPTION = 'local_mongo'; // this must go before importing db.js 5 | process.env.LOCAL_MONGO = 'localhost'; 6 | 7 | const exit = require('./exit'); 8 | const Hero = require('../server/services/mongo/hero.model'); 9 | const Villain = require('../server/services/mongo/villain.model'); 10 | const Settings = require('../server/services/mongo/settings.model'); 11 | const heroes = require('./heroes'); 12 | const villains = require('./villains'); 13 | const { mongoose, connect } = require('../server/services/mongo/db'); 14 | 15 | const captains = console; 16 | const settings = [{ name: 'Local Mongo' }]; 17 | 18 | connect(); 19 | 20 | go().then(() => { 21 | mongoose.connection.close(() => { 22 | console.log('Mongoose default connection is disconnected due to application termination'); 23 | process.exit(0); 24 | }); 25 | exit(`Completed successfully`); 26 | }) 27 | .catch((error) => { exit(`Completed with error ${JSON.stringify(error)}`); }); 28 | 29 | async function go() { 30 | await refreshDocuments(Hero, 'heroes', heroes); 31 | await refreshDocuments(Villain, 'villains', villains); 32 | await refreshDocuments(Settings, 'settings', settings); 33 | } 34 | 35 | async function refreshDocuments(model, collection, documents) { 36 | // Delete the collections 37 | return mongoose.connection.dropCollection(collection) 38 | // create the collections 39 | .then(() => mongoose.connection.createCollection(collection)) 40 | // Insert the items 41 | .then(() => model.insertMany(documents)) 42 | // Read the items 43 | .then(() => model.find({})) 44 | .then(docs => captains.log(`reading content: `, docs)); 45 | } 46 | -------------------------------------------------------------------------------- /meta/exit.js: -------------------------------------------------------------------------------- 1 | function exit(message) { 2 | console.log(message); 3 | console.log('Press any key to exit'); 4 | // process.stdin.setRawMode(true); 5 | process.stdin.resume(); 6 | process.stdin.on('data', process.exit.bind(process, 0)); 7 | } 8 | module.exports = exit; 9 | -------------------------------------------------------------------------------- /meta/heroes.js: -------------------------------------------------------------------------------- 1 | const heroes = [ 2 | { 3 | id: 'HeroAslaug', 4 | name: 'Aslaug', 5 | description: 'warrior queen', 6 | }, 7 | { 8 | id: 'HeroBjorn', 9 | name: 'Bjorn Ironside', 10 | description: 'king of 9th century Sweden', 11 | }, 12 | { 13 | id: 'HeroIvar', 14 | name: 'Ivar the Boneless', 15 | description: 'commander of the Great Heathen Army', 16 | }, 17 | { 18 | id: 'HeroLagertha', 19 | name: 'Lagertha the Shieldmaiden', 20 | description: 'aka Hlaðgerðr', 21 | }, 22 | { 23 | id: 'HeroRagnar', 24 | name: 'Ragnar Lothbrok', 25 | description: 'aka Ragnar Sigurdsson', 26 | }, 27 | { 28 | id: 'HeroThora', 29 | name: 'Thora Town-hart', 30 | description: 'daughter of Earl Herrauðr of Götaland', 31 | }, 32 | ]; 33 | 34 | module.exports = heroes; 35 | -------------------------------------------------------------------------------- /meta/villains.js: -------------------------------------------------------------------------------- 1 | const villains = [ 2 | { 3 | id: 'VillainJohn', 4 | name: 'John', 5 | description: 'Slayer of JavaScript' 6 | }, 7 | { 8 | id: 'VillainMadelyn', 9 | name: 'Madelyn', 10 | description: 'Wielder of the Service Worker' 11 | }, 12 | { 13 | id: 'VillainLandon', 14 | name: 'Landon', 15 | description: 'The Hacker of Node' 16 | } 17 | ]; 18 | 19 | module.exports = villains; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vikings", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve -o", 8 | "build": "ng build --prod", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "gen-stats": "ng build --prod --stats-json", 13 | "view-stats": "webpack-bundle-analyzer dist/stats.json", 14 | "map": "source-map-explorer dist/vikings/*es2015*.js*", 15 | "docker-up": "docker-compose up -d --build", 16 | "docker-up-debug": "docker-compose -f docker-compose.debug.yml up -d --build", 17 | "docker-down": "docker-compose down", 18 | "local-proxy": "concurrently \"npm run local\" \"ng serve --proxy-config proxy.conf.json\"", 19 | "local-proxy-debug": "concurrently \"npm run debug\" \"ng serve --proxy-config proxy.conf.json --open\"", 20 | "start-server": "node server.js", 21 | "start_debug": "node --inspect=0.0.0.0:9229 server.js", 22 | "start_azure_debug": "if [[ ${APPSVC_TUNNEL_PORT} != \"\" ]]; then node --inspect=0.0.0.0:$APPSVC_TUNNEL_PORT server.js; else npm run start; fi", 23 | "local": "WWW=./dist node -r dotenv/config server.js", 24 | "debug": "WWW=./dist node -r dotenv/config --inspect=9229 server.js" 25 | }, 26 | "private": true, 27 | "dependencies": { 28 | "@angular/animations": "~9.1.0", 29 | "@angular/cdk": "^9.2.0", 30 | "@angular/common": "~9.1.0", 31 | "@angular/compiler": "~9.1.0", 32 | "@angular/core": "~9.1.0", 33 | "@angular/forms": "~9.1.0", 34 | "@angular/material": "^9.2.0", 35 | "@angular/platform-browser": "~9.1.0", 36 | "@angular/platform-browser-dynamic": "~9.1.0", 37 | "@angular/router": "~9.1.0", 38 | "@azure/cosmos": "^2.0.4", 39 | "angular-in-memory-web-api": "^0.9.0", 40 | "body-parser": "^1.18.2", 41 | "express": "^4.16.4", 42 | "hammerjs": "^2.0.8", 43 | "mongodb": "^3.5.2", 44 | "mongoose": "^5.8.11", 45 | "rxjs": "~6.5.4", 46 | "tslib": "^1.10.0", 47 | "zone.js": "~0.10.2" 48 | }, 49 | "devDependencies": { 50 | "@angular-devkit/build-angular": "^0.901.0", 51 | "@angular/cli": "^9.1.0", 52 | "@angular/compiler-cli": "~9.1.0", 53 | "@angular/language-service": "~9.1.0", 54 | "@types/jasmine": "~3.5.0", 55 | "@types/jasminewd2": "~2.0.3", 56 | "@types/node": "^12.11.1", 57 | "codelyzer": "^5.1.2", 58 | "concurrently": "^5.1.0", 59 | "dotenv": "^8.2.0", 60 | "jasmine-core": "~3.5.0", 61 | "jasmine-spec-reporter": "~4.2.1", 62 | "karma": "~4.3.0", 63 | "karma-chrome-launcher": "~3.1.0", 64 | "karma-coverage-istanbul-reporter": "~2.1.0", 65 | "karma-jasmine": "~2.0.1", 66 | "karma-jasmine-html-reporter": "^1.4.2", 67 | "protractor": "^5.4.3", 68 | "source-map-explorer": "^2.3.1", 69 | "ts-node": "~8.3.0", 70 | "tslint": "~5.18.0", 71 | "typescript": "~3.7.5", 72 | "webpack-bundle-analyzer": "^3.6.1", 73 | "webpack-visualizer-plugin": "^0.1.11" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8626", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const server = require('./server/index'); 2 | 3 | server.start(); 4 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const routes = require('./routes'); 4 | 5 | const captains = console; 6 | 7 | function start() { 8 | if (!process.env.NODE_ENV || !process.env.PORT) { 9 | captains.error( 10 | 'ENV variables are missing.', 11 | 'Verify that you have set them directly or in a .env file.', 12 | ); 13 | process.exit(1); 14 | } else { 15 | captains.log('Using ENV variables'); 16 | } 17 | 18 | const app = express(); 19 | const port = process.env.PORT || 8626; 20 | const www = process.env.WWW || './'; 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | 24 | app.use(express.static(www)); 25 | captains.log(`serving ${www}`); 26 | app.use('/api', routes); 27 | app.get('*', (req, res) => { 28 | res.sendFile(`index.html`, { root: www }); 29 | }); 30 | app.listen(port, () => captains.log(`listening on http://localhost:${port}`)); 31 | } 32 | 33 | module.exports.start = start; 34 | -------------------------------------------------------------------------------- /server/routes/hero.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const services = require('../services'); 3 | 4 | const router = express.Router(); 5 | const { heroService } = services; 6 | 7 | router.get('/heroes', (req, res) => { 8 | heroService.getHeroes(req, res); 9 | }); 10 | 11 | router.post('/hero', (req, res) => { 12 | heroService.postHero(req, res); 13 | }); 14 | 15 | router.put('/hero/:id', (req, res) => { 16 | heroService.putHero(req, res); 17 | }); 18 | 19 | router.delete('/hero/:id', (req, res) => { 20 | heroService.deleteHero(req, res); 21 | }); 22 | 23 | // TODO: example of SQL query 24 | // Learn more here: https://www.documentdb.com/sql/demo 25 | // router.get('/hero/querybyname/:name', (req, res) => { 26 | // heroService.queryHeroesNyName(req, res); 27 | // }); 28 | 29 | module.exports = router; 30 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | 5 | router.use('/', require('./hero.routes')); 6 | router.use('/', require('./villain.routes')); 7 | router.use('/', require('./settings.routes')); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /server/routes/settings.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const services = require('../services'); 3 | 4 | const router = express.Router(); 5 | const { settingsService } = services; 6 | 7 | router.get('/settings', (req, res) => { 8 | settingsService.getSettings(req, res); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /server/routes/villain.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const services = require('../services'); 3 | 4 | const router = express.Router(); 5 | const { villainService } = services; 6 | 7 | router.get('/villains', (req, res) => { 8 | villainService.getVillains(req, res); 9 | }); 10 | 11 | router.post('/villain', (req, res) => { 12 | villainService.postVillain(req, res); 13 | }); 14 | 15 | router.put('/villain/:id', (req, res) => { 16 | villainService.putVillain(req, res); 17 | }); 18 | 19 | router.delete('/villain/:id', (req, res) => { 20 | villainService.deleteVillain(req, res); 21 | }); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /server/services/cosmos-sdk/config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // require('dotenv').config(); 3 | 4 | module.exports = { 5 | endpoint: process.env.CORE_API_URL, 6 | masterKey: process.env.CORE_API_KEY, 7 | 8 | databaseDefName: 'vikings-db', 9 | settingsContainer: 'settings', 10 | heroContainer: 'heroes', 11 | villainContainer: 'villains', 12 | }; 13 | -------------------------------------------------------------------------------- /server/services/cosmos-sdk/db.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const cosmos = require('@azure/cosmos'); 3 | const { endpoint, masterKey } = require('./config'); 4 | 5 | const { CosmosClient } = cosmos; 6 | const client = new CosmosClient({ endpoint, auth: { masterKey } }); 7 | 8 | module.exports = client; 9 | -------------------------------------------------------------------------------- /server/services/cosmos-sdk/hero.service.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const client = require('./db'); 3 | const { databaseDefName, heroContainer } = require('./config'); 4 | 5 | const container = client.database(databaseDefName).container(heroContainer); 6 | const captains = console; 7 | 8 | async function getHeroes(req, res) { 9 | try { 10 | const { result: heroes } = await container.items.readAll().toArray(); 11 | captains.log(`${heroes.length} Heroes retrieved successfully!`); 12 | res.status(200).json(heroes); 13 | } catch (error) { 14 | res.status(500).send(error); 15 | } 16 | } 17 | 18 | async function postHero(req, res) { 19 | const hero = { 20 | name: req.body.name, 21 | description: req.body.description, 22 | }; 23 | hero.id = `Hero ${hero.name}`; 24 | 25 | try { 26 | const { body } = await container.items.create(hero); 27 | res.status(201).json(body); 28 | captains.log(`Hero ${hero.name} created successfully!`); 29 | } catch (error) { 30 | res.status(500).send(error); 31 | } 32 | } 33 | 34 | async function putHero(req, res) { 35 | const hero = { 36 | id: req.params.id, 37 | name: req.body.name, 38 | description: req.body.description, 39 | }; 40 | 41 | try { 42 | const { body } = await container.items.upsert(hero); 43 | res.status(200).json(body); 44 | captains.log(`Hero ${hero.name} updated successfully!`); 45 | } catch (error) { 46 | res.status(500).send(error); 47 | } 48 | } 49 | 50 | async function deleteHero(req, res) { 51 | const { id } = req.params; 52 | 53 | try { 54 | const { body } = await container.item(id).delete(); 55 | res.status(200).json(body); 56 | captains.log(`Hero ${id} deleted successfully!`); 57 | } catch (error) { 58 | res.status(500).send(error); 59 | } 60 | } 61 | 62 | async function queryHeroesNyName(req, res) { 63 | console.log(`Querying container:\n${heroContainer}`); 64 | 65 | const querySpec = { 66 | query: 'SELECT h.id, h.name, h.description FROM heroes h WHERE h.name = @value', 67 | parameters: [ 68 | { 69 | name: '@value', 70 | value: req.params.name, // e.g. 'api/hero/querybyname/Aslaug', 71 | }, 72 | ], 73 | }; 74 | 75 | captains.log(querySpec); 76 | 77 | try { 78 | const { result: heroes } = await container.items.query(querySpec).toArray(); 79 | heroes.forEach((queryResult) => { 80 | const resultString = JSON.stringify(queryResult); 81 | captains.log(`Query returned ${resultString}\n`); 82 | }); 83 | res.status(200).json(heroes); 84 | } catch (error) { 85 | res.status(500).send(error); 86 | } 87 | } 88 | 89 | // function getHeroesViaPromises(req, res) { 90 | // container 91 | // .items.readAll() 92 | // .toArray() 93 | // .then(({ result: heroes }) => { 94 | // res.status(200).json(heroes); 95 | // }) 96 | // .catch(error => res.status(500).send(error)); 97 | // } 98 | 99 | module.exports = { 100 | getHeroes, 101 | postHero, 102 | putHero, 103 | deleteHero, 104 | queryHeroesNyName, 105 | }; 106 | -------------------------------------------------------------------------------- /server/services/cosmos-sdk/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const heroService = require('./hero.service'); 3 | const villainService = require('./villain.service'); 4 | const settingsService = require('./settings.service'); 5 | 6 | module.exports = { 7 | heroService, 8 | villainService, 9 | settingsService, 10 | }; 11 | -------------------------------------------------------------------------------- /server/services/cosmos-sdk/settings.service.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const client = require('./db'); 3 | const { databaseDefName, settingsContainer } = require('./config'); 4 | 5 | const container = client.database(databaseDefName).container(settingsContainer); 6 | 7 | async function getSettings(req, res) { 8 | try { 9 | const { result: settings } = await container.items.readAll().toArray(); 10 | res.status(200).json(settings); 11 | } catch (error) { 12 | res.status(500).send(error); 13 | } 14 | } 15 | 16 | module.exports = { 17 | getSettings, 18 | }; 19 | -------------------------------------------------------------------------------- /server/services/cosmos-sdk/villain.service.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const client = require('./db'); 3 | const { databaseDefName, villainContainer } = require('./config'); 4 | 5 | const container = client.database(databaseDefName).container(villainContainer); 6 | const captains = console; 7 | 8 | async function getVillains(req, res) { 9 | try { 10 | const { result: villains } = await container.items.readAll().toArray(); 11 | res.status(200).json(villains); 12 | } catch (error) { 13 | res.status(500).send(error); 14 | } 15 | } 16 | 17 | async function postVillain(req, res) { 18 | const villain = { 19 | name: req.body.name, 20 | description: req.body.description, 21 | }; 22 | villain.id = `Villain ${villain.name}`; 23 | 24 | try { 25 | const { body } = await container.items.create(villain); 26 | res.status(201).json(body); 27 | captains.log('Villain created successfully!'); 28 | } catch (error) { 29 | res.status(500).send(error); 30 | } 31 | } 32 | 33 | async function putVillain(req, res) { 34 | const villain = { 35 | id: req.params.id, 36 | name: req.body.name, 37 | description: req.body.description, 38 | }; 39 | 40 | try { 41 | const { body } = await container.items.upsert(villain); 42 | res.status(200).json(body); 43 | captains.log('Villain updated successfully!'); 44 | } catch (error) { 45 | res.status(500).send(error); 46 | } 47 | } 48 | 49 | async function deleteVillain(req, res) { 50 | const { id } = req.params; 51 | 52 | try { 53 | const { body } = await container.item(id).delete(); 54 | res.status(200).json(body); 55 | captains.log('Villain deleted successfully!'); 56 | } catch (error) { 57 | res.status(500).send(error); 58 | } 59 | } 60 | 61 | module.exports = { 62 | getVillains, 63 | postVillain, 64 | putVillain, 65 | deleteVillain, 66 | }; 67 | -------------------------------------------------------------------------------- /server/services/index.js: -------------------------------------------------------------------------------- 1 | const cosmosSDK = require('./cosmos-sdk'); 2 | const mongo = require('./mongo'); 3 | const { connect: connectToMongo } = require('./mongo/db'); 4 | 5 | let useCosmosSDK = false; 6 | 7 | const captains = console; 8 | 9 | switch (process.env.DATA_OPTION) { 10 | case 'cloud_cosmos_sdk': 11 | useCosmosSDK = true; 12 | captains.log(`===> Using Cosmos with the Cosmos SDK`); 13 | break; 14 | 15 | case 'cloud_cosmos': 16 | useCosmosSDK = false; 17 | captains.log(`===> Using Cosmos with the Mongo API`); 18 | connectToMongo(); 19 | break; 20 | 21 | case 'local_mongo': 22 | useCosmosSDK = false; 23 | captains.log(`===> Using Mongo`); 24 | connectToMongo(); 25 | break; 26 | 27 | default: 28 | captains.log(`===> Database not selected`); 29 | break; 30 | } 31 | 32 | module.exports = { 33 | heroService: useCosmosSDK ? cosmosSDK.heroService : mongo.heroService, 34 | villainService: useCosmosSDK ? cosmosSDK.villainService : mongo.villainService, 35 | settingsService: useCosmosSDK ? cosmosSDK.settingsService : mongo.settingsService, 36 | }; 37 | -------------------------------------------------------------------------------- /server/services/mongo/config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // require('dotenv').config(); 3 | 4 | module.exports = { 5 | mongoApiAccount: process.env.MONGO_API_ACCOUNT, 6 | mongoApiPort: process.env.MONGO_API_PORT, 7 | mongoDb: process.env.MONGO_API_DB, 8 | mongoApiKey: process.env.MONGO_API_KEY, 9 | 10 | localMongo: process.env.LOCAL_MONGO || 'localhost', 11 | }; 12 | -------------------------------------------------------------------------------- /server/services/mongo/db.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mongoose = require('mongoose'); 3 | const { 4 | mongoApiKey, 5 | mongoApiAccount, 6 | mongoApiPort, 7 | mongoDb, 8 | localMongo, 9 | } = require('./config'); 10 | /** 11 | * Set to Node.js native promises 12 | * Per http://mongoosejs.com/docs/promises.html 13 | */ 14 | mongoose.Promise = global.Promise; 15 | 16 | const captains = console; 17 | const key = encodeURIComponent(mongoApiKey); 18 | const mongoOnCosmosUri = `mongodb://${mongoApiAccount}:${key}@${mongoApiAccount}.documents.azure.com:${mongoApiPort}/${mongoDb}?ssl=true`; 19 | const mongoUri = `mongodb://${localMongo}:27017/${mongoDb}`; 20 | let dbUri = ''; 21 | 22 | captains.log(`process.env.DATA_OPTION=${process.env.DATA_OPTION}`); 23 | if (process.env.DATA_OPTION === 'local_mongo') { 24 | dbUri = mongoUri; 25 | } else { 26 | dbUri = mongoOnCosmosUri; 27 | } 28 | 29 | function connectWithRetry() { 30 | if (process.env.USE_LIVE_DATA === 'yes') { 31 | mongoose.set('debug', true); 32 | return mongoose.connect( 33 | dbUri, 34 | { useNewUrlParser: true }, 35 | // ).then(() => { 36 | // console.log('MongoDB is connected'); 37 | // return true; 38 | // } 39 | ).catch((err) => { 40 | console.log('MongoDB connection unsuccessful, retry after 5 seconds.', err); 41 | setTimeout(connectWithRetry, 5000); 42 | }); 43 | } 44 | return false; 45 | } 46 | 47 | function connect() { 48 | connectWithRetry(); 49 | } 50 | 51 | process.on('SIGINT', () => { 52 | // If the Node process ends, close the Mongoose connection 53 | mongoose.connection.close(() => { 54 | console.log('Mongoose default connection is disconnected due to application termination'); 55 | process.exit(0); 56 | }); 57 | }); 58 | 59 | module.exports = { mongoose, connect }; 60 | -------------------------------------------------------------------------------- /server/services/mongo/hero.model.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mongoose = require('mongoose'); 3 | 4 | const { Schema } = mongoose; 5 | const heroSchema = new Schema( 6 | { 7 | id: String, 8 | name: String, 9 | description: String, 10 | }, 11 | { 12 | collection: 'heroes', 13 | read: 'nearest', 14 | }, 15 | ); 16 | const Hero = mongoose.model('Hero', heroSchema); 17 | module.exports = Hero; 18 | -------------------------------------------------------------------------------- /server/services/mongo/hero.service.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { ReadPreference } = require('mongodb'); 3 | const Hero = require('./hero.model'); 4 | 5 | const captains = console; 6 | 7 | function getHeroes(req, res) { 8 | const docquery = Hero.find({}).read(ReadPreference.NEAREST); 9 | docquery 10 | .exec() 11 | .then(heroes => res.status(200).json(heroes)) 12 | .catch(error => res.status(500).send(error)); 13 | } 14 | 15 | function postHero(req, res) { 16 | const originalHero = { 17 | name: req.body.name, 18 | description: req.body.description, 19 | }; 20 | originalHero.id = `Hero${originalHero.name}`; 21 | const hero = new Hero(originalHero); 22 | hero.save((error) => { 23 | if (checkServerError(res, error)) return; 24 | res.status(201).json(hero); 25 | captains.log('Hero created successfully!'); 26 | }); 27 | } 28 | 29 | function putHero(req, res) { 30 | const updatedHero = { 31 | id: req.params.id, 32 | name: req.body.name, 33 | description: req.body.description, 34 | }; 35 | 36 | Hero.findOneAndUpdate( 37 | { id: updatedHero.id }, 38 | { $set: updatedHero }, 39 | { upsert: true, new: true }, 40 | (error, doc) => { 41 | if (checkServerError(res, error)) return; 42 | res.status(200).json(doc); 43 | captains.log('Hero updated successfully!'); 44 | }, 45 | ); 46 | } 47 | 48 | function deleteHero(req, res) { 49 | const { id } = req.params; 50 | Hero.findOneAndRemove({ id }) 51 | .then((hero) => { 52 | if (!checkFound(res, hero)) return; 53 | res.status(200).json(hero); 54 | captains.log('Hero deleted successfully!'); 55 | }) 56 | .catch(error => checkServerError(res, error)); 57 | } 58 | 59 | 60 | function checkServerError(res, error) { 61 | if (error) { 62 | res.status(500).send(error); 63 | return error; 64 | } 65 | return false; 66 | } 67 | 68 | function checkFound(res, hero) { 69 | if (!hero) { 70 | res.status(404).send('Hero not found.'); 71 | return false; 72 | } 73 | return hero; 74 | } 75 | 76 | 77 | module.exports = { 78 | getHeroes, 79 | postHero, 80 | putHero, 81 | deleteHero, 82 | }; 83 | -------------------------------------------------------------------------------- /server/services/mongo/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const heroService = require('./hero.service'); 3 | const villainService = require('./villain.service'); 4 | const settingsService = require('./settings.service'); 5 | 6 | module.exports = { 7 | heroService, 8 | villainService, 9 | settingsService, 10 | }; 11 | -------------------------------------------------------------------------------- /server/services/mongo/settings.model.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mongoose = require('mongoose'); 3 | 4 | const { Schema } = mongoose; 5 | const settingsSchema = new Schema( 6 | { 7 | name: String, 8 | }, 9 | { 10 | collection: 'settings', 11 | read: 'nearest', 12 | }, 13 | ); 14 | const Settings = mongoose.model('Settings', settingsSchema); 15 | module.exports = Settings; 16 | -------------------------------------------------------------------------------- /server/services/mongo/settings.service.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { ReadPreference } = require('mongodb'); 3 | const Settings = require('./settings.model'); 4 | 5 | function getSettings(req, res) { 6 | const docquery = Settings.find({}).read(ReadPreference.NEAREST); 7 | docquery 8 | .exec() 9 | .then(settings => res.status(200).json(settings)) 10 | .catch(error => res.status(500).send(error)); 11 | } 12 | 13 | module.exports = { 14 | getSettings, 15 | }; 16 | -------------------------------------------------------------------------------- /server/services/mongo/villain.model.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const mongoose = require('mongoose'); 3 | 4 | const { Schema } = mongoose; 5 | const villainSchema = new Schema( 6 | { 7 | id: String, 8 | name: String, 9 | description: String, 10 | }, 11 | { 12 | collection: 'villains', 13 | read: 'nearest', 14 | }, 15 | ); 16 | 17 | const Villain = mongoose.model('Villain', villainSchema); 18 | 19 | module.exports = Villain; 20 | -------------------------------------------------------------------------------- /server/services/mongo/villain.service.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { ReadPreference } = require('mongodb'); 3 | const Villain = require('./villain.model'); 4 | 5 | const captains = console; 6 | 7 | function getVillains(req, res) { 8 | const docquery = Villain.find({}).read(ReadPreference.NEAREST); 9 | docquery 10 | .exec() 11 | .then(villains => res.status(200).json(villains)) 12 | .catch(error => res.status(500).send(error)); 13 | } 14 | 15 | function postVillain(req, res) { 16 | const originalVillain = { 17 | name: req.body.name, 18 | description: req.body.description, 19 | }; 20 | originalVillain.id = `Villain${originalVillain.name}`; 21 | const villain = new Villain(originalVillain); 22 | villain.save((error) => { 23 | if (checkServerError(res, error)) return; 24 | res.status(201).json(villain); 25 | captains.log('Villain created successfully!'); 26 | }); 27 | } 28 | 29 | function putVillain(req, res) { 30 | const updatedVillain = { 31 | id: req.params.id, 32 | name: req.body.name, 33 | description: req.body.description, 34 | }; 35 | 36 | Villain.findOneAndUpdate( 37 | { id: updatedVillain.id }, 38 | { $set: updatedVillain }, 39 | { upsert: true, new: true }, 40 | (error, doc) => { 41 | if (checkServerError(res, error)) return; 42 | res.status(200).json(doc); 43 | captains.log('Villain updated successfully!'); 44 | }, 45 | ); 46 | } 47 | 48 | function deleteVillain(req, res) { 49 | const { id } = req.params; 50 | Villain.findOneAndRemove({ id }) 51 | .then((villain) => { 52 | if (!checkFound(res, villain)) return; 53 | res.status(200).json(villain); 54 | captains.log('Villain deleted successfully!'); 55 | }) 56 | .catch(error => checkServerError(res, error)); 57 | } 58 | 59 | 60 | function checkServerError(res, error) { 61 | if (error) { 62 | res.status(500).send(error); 63 | return error; 64 | } 65 | return false; 66 | } 67 | 68 | function checkFound(res, villain) { 69 | if (!villain) { 70 | res.status(404).send('Villain not found.'); 71 | return false; 72 | } 73 | return villain; 74 | } 75 | 76 | 77 | module.exports = { 78 | getVillains, 79 | postVillain, 80 | putVillain, 81 | deleteVillain, 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/app-dev.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppModule } from './app.module'; 4 | import { AppComponent } from './app.component'; 5 | 6 | import { 7 | HttpClientInMemoryWebApiModule, 8 | InMemoryDbService 9 | } from 'angular-in-memory-web-api'; 10 | import { InMemoryDataService } from './core'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | AppModule, 15 | // The HttpClientInMemoryWebApiModule module intercepts HTTP requests 16 | // and returns simulated server responses. 17 | HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { 18 | dataEncapsulation: false, 19 | delay: 300, 20 | passThruUnknownUrl: true 21 | }) 22 | ], 23 | providers: [{ provide: InMemoryDataService, useExisting: InMemoryDbService }], 24 | bootstrap: [AppComponent] 25 | }) 26 | export class AppDevModule {} 27 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | margin: 0px; 3 | // flex-flow: column wrap; 4 | display: flex; 5 | min-height: 100vh; 6 | flex-direction: column; 7 | } 8 | .content { 9 | flex: 1; 10 | margin: 0.5em; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [ 8 | RouterTestingModule 9 | ], 10 | declarations: [ 11 | AppComponent 12 | ], 13 | }).compileComponents(); 14 | })); 15 | it('should create the app', async(() => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.debugElement.componentInstance; 18 | expect(app).toBeTruthy(); 19 | })); 20 | it(`should have as title 'vk'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('vk'); 24 | })); 25 | it('should render title in a h1 tag', async(() => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to vk!'); 30 | })); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | import { openCloseAnimation, routeSlideInAnimation } from './core/animations'; 4 | 5 | @Component({ 6 | selector: 'vk-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'], 9 | animations: [routeSlideInAnimation, openCloseAnimation], 10 | }) 11 | export class AppComponent { 12 | prepareRoute(outlet: RouterOutlet) { 13 | return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | 5 | import { routes } from './routes'; 6 | import { AppComponent } from './app.component'; 7 | import { 8 | ModalComponent, 9 | ToolbarComponent, 10 | httpInterceptorProviders 11 | } from './core'; 12 | import { HomeComponent } from './home/home.component'; 13 | import { RouterModule } from '@angular/router'; 14 | import { CommonModule } from '@angular/common'; 15 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 16 | import { SharedModule } from './shared/shared.module'; 17 | import { NgMaterialModule } from './ng-material/ng-material.module'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | BrowserModule, 22 | BrowserAnimationsModule, 23 | HttpClientModule, 24 | NgMaterialModule, 25 | SharedModule, 26 | RouterModule.forRoot(routes) 27 | ], 28 | declarations: [AppComponent, HomeComponent, ToolbarComponent, ModalComponent], 29 | providers: [httpInterceptorProviders], 30 | bootstrap: [AppComponent] 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /src/app/core/animations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | animateChild, 4 | group, 5 | query, 6 | sequence, 7 | stagger, 8 | style, 9 | transition, 10 | trigger, 11 | } from '@angular/animations'; 12 | 13 | export const routeSlideInAnimation = trigger('routeSlide', [ 14 | transition('* => *', [ 15 | style({ position: 'relative' }), 16 | query( 17 | ':enter, :leave', 18 | [ 19 | style({ 20 | position: 'absolute', 21 | top: 0, 22 | left: 0, 23 | width: '100%', 24 | }), 25 | ], 26 | { optional: true }, 27 | ), 28 | query(':enter', [style({ left: '-100%' })], { optional: true }), 29 | query(':leave', animateChild(), { optional: true }), 30 | group([ 31 | query(':leave', [animate('500ms ease-out', style({ left: '100%' }))], { optional: true }), 32 | query(':enter', [animate('500ms ease-out', style({ left: '0%' }))], { optional: true }), 33 | ]), 34 | query(':enter', animateChild(), { optional: true }), 35 | ]), 36 | ]); 37 | 38 | export const openCloseAnimation = trigger('openClose', [ 39 | // state('show', style({ opacity: '1', transform: 'scale(1)' })), 40 | // state('hide', style({ opacity: '0.2', transform: 'scale(0.5)' })), 41 | transition('void => *', [ 42 | sequence([ 43 | // 'From' styles 44 | style({ 45 | opacity: 0.2, 46 | transform: 'scale(0.5)', 47 | }), 48 | animate( 49 | '300ms ease-in', 50 | // 'To' styles 51 | // 1 - Comment this to remove the item's grow... 52 | style({ 53 | opacity: 1, 54 | transform: 'scale(1.1)', 55 | }), 56 | ), 57 | animate( 58 | '300ms ease-in', 59 | // 'To' styles 60 | // 1 - Comment this to remove the item's grow... 61 | style({ 62 | transform: 'scale(1.0)', 63 | }), 64 | ), 65 | ]), 66 | ]), 67 | transition('* => void', [ 68 | animate( 69 | '500ms ease-out', 70 | // 'To' Style 71 | style({ opacity: 0, transform: 'scale(0.5)' }), 72 | ), 73 | ]), 74 | ]); 75 | 76 | export const flyInOutAnimation = trigger('flyInOut', [ 77 | transition('void => *', [style({ transform: 'translateX(-100%)' }), animate('250ms ease-out')]), 78 | transition('* => void', [animate('250ms ease-out', style({ transform: 'translateX(100%)' }))]), 79 | ]); 80 | 81 | export const staggerListAnimation = trigger('staggerList', [ 82 | transition('* <=> *', [ 83 | query( 84 | ':enter', 85 | [ 86 | style({ opacity: 0, transform: 'translateY(-15px)' }), 87 | stagger( 88 | '50ms', 89 | animate('550ms ease-out', style({ opacity: 1, transform: 'translateY(0px)' })), 90 | ), 91 | ], 92 | { optional: true }, 93 | ), 94 | query(':leave', animate('50ms', style({ opacity: 0 })), { 95 | optional: true, 96 | }), 97 | ]), 98 | ]); 99 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | // import { CommonModule } from '@angular/common'; 2 | // import { NgModule, Optional, SkipSelf } from '@angular/core'; 3 | // import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | // import { RouterModule } from '@angular/router'; 5 | // import { NgMaterialModule } from '../ng-material/ng-material.module'; 6 | // import { SharedModule } from '../shared/shared.module'; 7 | // import { ModalComponent } from './modal/modal.component'; 8 | // import { throwIfAlreadyLoaded } from './module-import-check'; 9 | // import { ToolbarComponent } from './toolbar/toolbar.component'; 10 | // import { httpInterceptorProviders } from './interceptors'; 11 | 12 | // @NgModule({ 13 | // imports: [ 14 | // CommonModule, 15 | // BrowserAnimationsModule, 16 | // SharedModule, 17 | // NgMaterialModule, 18 | // RouterModule // because we use and routerLink 19 | // ], 20 | // declarations: [ToolbarComponent, ModalComponent], 21 | // providers: [httpInterceptorProviders], 22 | // exports: [BrowserAnimationsModule, ToolbarComponent, NgMaterialModule], 23 | // }) 24 | // export class CoreModule { 25 | // constructor( 26 | // @Optional() 27 | // @SkipSelf() 28 | // parentModule: CoreModule 29 | // ) { 30 | // throwIfAlreadyLoaded(parentModule, 'CoreModule'); 31 | // } 32 | // } 33 | -------------------------------------------------------------------------------- /src/app/core/in-memory-data.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Hero-oriented InMemoryDbService with method overrides. 3 | */ 4 | import { Injectable } from '@angular/core'; 5 | import { 6 | ParsedRequestUrl, 7 | RequestInfo, 8 | RequestInfoUtilities 9 | } from 'angular-in-memory-web-api'; 10 | 11 | /** In-memory database data */ 12 | interface Db { 13 | [collectionName: string]: any[]; 14 | } 15 | 16 | @Injectable() 17 | export class InMemoryDataService { 18 | /** True if in-mem service is intercepting; all requests pass thru when false. */ 19 | active = true; 20 | 21 | /** In-memory database data */ 22 | db: Db = {}; 23 | 24 | /** Create the in-memory database on start or by command */ 25 | createDb(reqInfo?: RequestInfo) { 26 | this.db = getDbData(); 27 | 28 | if (reqInfo) { 29 | const body = reqInfo.utils.getJsonBody(reqInfo.req) || {}; 30 | if (body.clear === true) { 31 | // tslint:disable-next-line:forin 32 | for (const coll in this.db) { 33 | this.db[coll].length = 0; 34 | } 35 | } 36 | 37 | this.active = !!body.active; 38 | } 39 | return this.db; 40 | } 41 | 42 | /** 43 | * Override `parseRequestUrl` 44 | * Manipulates the request URL or the parsed result. 45 | * If in-mem is inactive, clear collectionName so that service passes request thru. 46 | * If in-mem is active, after parsing with the default parser, 47 | * @param url from request URL 48 | * @param utils for manipulating parsed URL 49 | */ 50 | parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl { 51 | const parsed = utils.parseRequestUrl(url); 52 | parsed.collectionName = this.active 53 | ? mapCollectionName(parsed.collectionName) 54 | : undefined; 55 | return parsed; 56 | } 57 | } 58 | 59 | /** 60 | * Remap a known singular collection name ("hero") 61 | * to the plural collection name ("heroes"); else return the name 62 | * @param name - collection name from the parsed URL 63 | */ 64 | function mapCollectionName(name: string): string { 65 | return ( 66 | ({ 67 | hero: 'heroes', 68 | villain: 'villains' 69 | } as any)[name] || name 70 | ); 71 | } 72 | 73 | /** 74 | * Development data 75 | */ 76 | function getDbData() { 77 | const heroes: any[] = [ 78 | { 79 | id: 'HeroRagnar', 80 | name: 'Ragnar Lothbrok', 81 | description: 'aka Ragnar Sigurdsson' 82 | }, 83 | { 84 | id: 'HeroIvar', 85 | name: 'Ivar the Boneless', 86 | description: 'commander of the Great Heathen Army' 87 | }, 88 | { 89 | id: 'HeroBjorn', 90 | name: 'Bjorn Ironside', 91 | description: 'king of 9th century Sweden' 92 | }, 93 | { 94 | id: 'HeroLagertha', 95 | name: 'Lagertha the Shieldmaiden', 96 | description: 'aka Hlaðgerðr' 97 | }, 98 | { 99 | id: 'HeroAslaug', 100 | name: 'Aslaug', 101 | description: 'warrior queen' 102 | }, 103 | { 104 | id: 'HeroThora', 105 | name: 'Thora Town-hart', 106 | description: 'daughter of Earl Herrauðr of Götaland' 107 | } 108 | ]; 109 | 110 | const villains: any[] = [ 111 | { 112 | id: 'VillainJohn', 113 | name: 'John', 114 | description: 'Slayer of JavaScript' 115 | }, 116 | { 117 | id: 'VillainMadelyn', 118 | name: 'Madelyn', 119 | description: 'Wielder of the Service Worker' 120 | }, 121 | { 122 | id: 'VillainLandon', 123 | name: 'Landon', 124 | description: 'The Hacker of Node' 125 | } 126 | ]; 127 | 128 | const settings: any[] = [ 129 | { 130 | name: 'In Memory API' 131 | } 132 | ]; 133 | 134 | return { heroes, villains, settings } as Db; 135 | } 136 | -------------------------------------------------------------------------------- /src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './in-memory-data.service'; 2 | export * from './interceptors'; 3 | export * from './modal/modal.component'; 4 | export * from './toolbar/toolbar.component'; 5 | export * from './model'; 6 | export * from './toast.service'; 7 | -------------------------------------------------------------------------------- /src/app/core/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpEvent, 4 | HttpInterceptor, 5 | HttpHandler, 6 | HttpRequest 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { AuthService } from './auth.service'; 10 | 11 | @Injectable() 12 | export class AuthInterceptor implements HttpInterceptor { 13 | constructor(private auth: AuthService) {} 14 | 15 | intercept( 16 | req: HttpRequest, 17 | next: HttpHandler 18 | ): Observable> { 19 | const authHeader = this.auth.getAuthorizationToken(); 20 | // const authHeader = 'Basic your-token-goes-here'; 21 | const authReq = req.clone({ 22 | setHeaders: { 23 | Authorization: authHeader, 24 | 'Content-Type': 'application/json' 25 | } 26 | }); 27 | 28 | console.log(`HTTP: Adding headers`); 29 | // Pass on the cloned request instead of the original request. 30 | return next.handle(authReq); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/core/interceptors/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class AuthService { 5 | getAuthorizationToken() { 6 | return [ 7 | 'Basic your-token-goes-here' 8 | // 'Authorization': 'Basic d2VudHdvcnRobWFuOkNoYW5nZV9tZQ==', 9 | // 'Accept': 'application/json;odata=verbose' 10 | ]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/interceptors/csrf.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class CSRFInterceptor implements HttpInterceptor { 7 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 8 | // let headers = req.headers; 9 | // // if (req.url.indexOf('http://your-url.azurewebsites.net') > -1) { 10 | // headers = req.headers.append('x-csrf-token', 'your-csrf-token-goes-here'); 11 | // // } 12 | // const clonedReq = req.clone({ headers }); 13 | const clonedReq = req.clone({ setHeaders: { 'x-csrf-token': 'your-csrf-token-goes-here' } }); 14 | console.log(`HTTP: Adding CSRF`); 15 | return next.handle(clonedReq); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/interceptors/ensure-ssl.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class EnsureSSLInterceptor implements HttpInterceptor { 7 | /** 8 | * Credit: https://angular.io/guide/http#http-interceptors 9 | */ 10 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 11 | // clone request and replace 'http://' with 'https://' at the same time 12 | const secureReq = req.clone({ 13 | url: req.url.replace('http://', 'https://') 14 | }); 15 | // send the cloned, "secure" request to the next handler. 16 | return next.handle(secureReq); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/core/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { AuthInterceptor } from './auth.interceptor'; 3 | import { CSRFInterceptor } from './csrf.interceptor'; 4 | import { TransformResponseInterceptor } from './transform-response.interceptor'; 5 | import { LogResponseTimeInterceptor } from './log-response.interceptor'; 6 | import { LogHeadersInterceptor } from './log-headers.interceptor'; 7 | import { EnsureSSLInterceptor } from './ensure-ssl.interceptor'; 8 | 9 | export * from './auth.interceptor'; 10 | export * from './csrf.interceptor'; 11 | export * from './ensure-ssl.interceptor'; 12 | export * from './log-headers.interceptor'; 13 | export * from './log-response.interceptor'; 14 | export * from './transform-response.interceptor'; 15 | 16 | export const httpInterceptorProviders = [ 17 | { provide: HTTP_INTERCEPTORS, useClass: EnsureSSLInterceptor, multi: true }, 18 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 19 | { provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true }, 20 | { provide: HTTP_INTERCEPTORS, useClass: TransformResponseInterceptor, multi: true }, 21 | { provide: HTTP_INTERCEPTORS, useClass: LogResponseTimeInterceptor, multi: true }, 22 | { provide: HTTP_INTERCEPTORS, useClass: LogHeadersInterceptor, multi: true } 23 | ]; 24 | 25 | /** 26 | * https://angular.io/guide/http#http-interceptors 27 | * 28 | * Why you care? 29 | * Have you ever needed to add headers to all or a subset of http requests? Transform the response? Log specific requests? 30 | * Without interception, developers would have to implement these tasks explicitly for each HttpClient method call. 31 | * 32 | * What is an interceptor? 33 | * HTTP Interception is a major feature of @angular/common/http. With interception, you declare interceptors that inspect and transform HTTP requests from your application to the server. The same interceptors may also inspect and transform the server's responses on their way back to the application. Multiple interceptors form a forward-and-backward chain of request/response handlers. 34 | * Interceptors can perform a variety of implicit tasks, from authentication to logging, in a routine, standard way, for every HTTP request/response. 35 | * 36 | * Providing Interceptors 37 | * Because interceptors are (optional) dependencies of the HttpClient service, you must provide them in the same injector (or a parent of the injector) that provides HttpClient. Interceptors provided after DI creates the HttpClient are ignored. 38 | * Use a barrel and export an array of the interceptors. 39 | * 40 | * Modifying Requests 41 | * Although interceptors are capable of mutating requests and responses, the HttpRequest and HttpResponse instance properties are readonly, rendering them largely immutable. 42 | * The request is immutable ... you must clone it. 43 | * Most interceptors transform the outgoing request before passing it to the next interceptor in the chain, by calling next.handle(transformedReq). An interceptor may transform the response event stream as well, by applying additional RxJS operators on the stream returned by next.handle(). 44 | * 45 | * Modifing Responses 46 | * After you return stream via next.handle(), you can pipe the stream's response. Use RxJS operators to transform or do what you will to it. 47 | * The response is immutable ... you must clone it. 48 | * 49 | * What is multi true? 50 | * The multi: true option is a required setting that tells Angular that HTTP_INTERCEPTORS is a token for a multiprovider that injects an array of values, rather than a single value. 51 | * 52 | * Order is important 53 | * Angular applies interceptors in the order that you provide them. If you provide interceptors A, then B, then C, requests will flow in A->B->C and responses will flow out C->B->A. 54 | 55 | 56 | 57 | */ 58 | -------------------------------------------------------------------------------- /src/app/core/interceptors/log-headers.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent } from '@angular/common/http'; 2 | import { Observable } from 'rxjs'; 3 | import { Injectable } from "@angular/core"; 4 | 5 | @Injectable() 6 | export class LogHeadersInterceptor implements HttpInterceptor { 7 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 8 | console.log(`HTTP: Log headers:`); 9 | let headerList: { key: string; values: string }[] = []; 10 | req.headers.keys().map(key => { 11 | headerList.push({ key, values: req.headers.getAll(key).toString() }); 12 | }); 13 | console.table(headerList); 14 | return next.handle(req); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/core/interceptors/log-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpInterceptor, 3 | HttpHandler, 4 | HttpRequest, 5 | HttpEvent, 6 | HttpResponse 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { tap, finalize } from 'rxjs/operators'; 10 | import { Injectable } from "@angular/core"; 11 | 12 | @Injectable() 13 | export class LogResponseTimeInterceptor implements HttpInterceptor { 14 | /** 15 | * Credit: https://angular.io/guide/http#http-interceptors 16 | */ 17 | // intercept_alternative(req: HttpRequest, next: HttpHandler): Observable> { 18 | // const started = Date.now(); 19 | // return next.handle(req).pipe( 20 | // tap(event => { 21 | // if (event instanceof HttpResponse) { 22 | // const elapsed = Date.now() - started; 23 | // console.log(`HTTP: Request for ${req.urlWithParams} took ${elapsed} ms.`); 24 | // } 25 | // }) 26 | // ); 27 | // } 28 | 29 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 30 | const started = Date.now(); 31 | let ok: string; 32 | 33 | return next.handle(req).pipe( 34 | tap( 35 | // Succeeds when there is a response; ignore other events 36 | event => (ok = event instanceof HttpResponse ? 'succeeded' : ''), 37 | // Operation failed; error is an HttpErrorResponse 38 | error => (ok = 'failed') 39 | ), 40 | // Log when response observable either completes or errors 41 | finalize(() => { 42 | const elapsed = Date.now() - started; 43 | console.log(`${req.method} "${req.urlWithParams}" \n\t ${ok} in ${elapsed} ms.`); 44 | }) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/core/interceptors/transform-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpInterceptor, 3 | HttpHandler, 4 | HttpRequest, 5 | HttpEvent, 6 | HttpResponse 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators'; 10 | import { Injectable } from "@angular/core"; 11 | 12 | @Injectable() 13 | export class TransformResponseInterceptor implements HttpInterceptor { 14 | intercept( 15 | req: HttpRequest, 16 | next: HttpHandler 17 | ): Observable> { 18 | return next.handle(req).pipe( 19 | map(event => { 20 | if (event instanceof HttpResponse) { 21 | if ( 22 | event.url && 23 | event.url.indexOf('heroes') >= 0 && 24 | Array.isArray(event.body) 25 | ) { 26 | let body = event.body.map(hero => { 27 | if (hero.name.match(/Aslaug/i)) { 28 | hero.name = 'Rey Skywalker'; 29 | } 30 | return hero; 31 | }); 32 | console.log(`HTTP: Request transformed`); 33 | return event.clone({ body }); 34 | } 35 | return event.clone(); // undefined means dont change it 36 | // return event.clone(undefined); // undefined means dont change it 37 | } 38 | }) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/core/modal/modal.component.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 | 3 | 4 |

{{message}}

5 |
6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/app/core/modal/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'vk-modal', 6 | templateUrl: './modal.component.html' 7 | }) 8 | export class ModalComponent implements OnInit { 9 | message: string; 10 | title: string; 11 | 12 | constructor( 13 | private dialogRef: MatDialogRef, 14 | @Inject(MAT_DIALOG_DATA) data 15 | ) { 16 | this.title = data.title; 17 | this.message = data.message; 18 | } 19 | ngOnInit() {} 20 | 21 | ok() { 22 | this.dialogRef.close(true); 23 | } 24 | 25 | cancel() { 26 | this.dialogRef.close(false); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/core/model/hero.ts: -------------------------------------------------------------------------------- 1 | export class Hero { 2 | id: string; 3 | name: string; 4 | description: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hero'; 2 | export * from './villain'; 3 | -------------------------------------------------------------------------------- /src/app/core/model/villain.ts: -------------------------------------------------------------------------------- 1 | export class Villain { 2 | id: string; 3 | name: string; 4 | description: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/module-import-check.ts: -------------------------------------------------------------------------------- 1 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { 2 | if (parentModule) { 3 | const msg = `${moduleName} has already been loaded. Import Core modules in the AppModule only.`; 4 | throw new Error(msg); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/core/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { map, catchError } from 'rxjs/operators'; 4 | import { of } from 'rxjs'; 5 | 6 | const api = '/api'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class SettingsService { 10 | constructor(private http: HttpClient) {} 11 | 12 | getSettings() { 13 | return this.http.get(`${api}/settings`).pipe( 14 | map(settings => settings[0] || {}), 15 | catchError(e => of({ name: 'In Memory API' })) 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/core/toast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class ToastService { 6 | constructor(public snackBar: MatSnackBar) {} 7 | 8 | openSnackBar(message: string, action: string) { 9 | setTimeout( 10 | () => this.snackBar.open(message, action, { duration: 2000 }), 11 | 0 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/core/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/core/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .pull-right { 4 | position: fixed !important; 5 | right: 8px; 6 | } 7 | 8 | .ng-title-icon > i { 9 | background-image: url('../../../assets/azureappservice.png'); 10 | background-repeat: no-repeat; 11 | background-position: center center; 12 | padding: 1.2em; 13 | } 14 | 15 | .title { 16 | margin-right: 1em; 17 | } 18 | 19 | .item { 20 | margin-left: 16px; 21 | } 22 | 23 | .item, 24 | a { 25 | vertical-align: middle; 26 | } 27 | 28 | .router-link-active { 29 | @include primary-background-contrast-color; 30 | } 31 | 32 | .footer-container { 33 | display: flex; 34 | flex-flow: row wrap; 35 | width: 100%; 36 | > * { 37 | // padding: 10px; 38 | // flex: 1 100%; 39 | display: flex; 40 | } 41 | } 42 | 43 | .commands { 44 | flex: 18 0px; 45 | order: 1; 46 | align-self: center; 47 | padding-left: 8px; 48 | } 49 | 50 | .avatar { 51 | flex: 1 auto; 52 | } 53 | 54 | .spacer { 55 | order: 0; 56 | flex: 18 0px; 57 | } 58 | 59 | .avatar-1 { 60 | order: 1; 61 | } 62 | 63 | .avatar-2 { 64 | order: 2; 65 | } 66 | 67 | .avatar-1 > i, 68 | .avatar-2 > i { 69 | background-repeat: no-repeat; 70 | background-position: center center; 71 | padding: 16px; 72 | 73 | border: 0; 74 | border-radius: 50%; 75 | opacity: 1; 76 | } 77 | .avatar-1 > i { 78 | background-image: url('../../../assets/wb.jpg'); 79 | } 80 | .avatar-2 > i { 81 | background-image: url('../../../assets/jp.jpg'); 82 | } 83 | 84 | .row-commands { 85 | height: 40px; 86 | border-top: 1px solid white; 87 | // @include primary-background-contrast-color; // end 88 | @include accent-background-color; // begin 89 | } 90 | -------------------------------------------------------------------------------- /src/app/core/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { SettingsService } from '../settings.service'; 3 | 4 | @Component({ 5 | selector: 'vk-toolbar', 6 | templateUrl: './toolbar.component.html', 7 | styleUrls: ['./toolbar.component.scss'], 8 | }) 9 | export class ToolbarComponent implements OnInit { 10 | labTitle = 'Vikings'; 11 | 12 | labState = 'Welcome USA!'; 13 | 14 | dbName = ''; 15 | 16 | constructor(private settingsService: SettingsService) {} 17 | 18 | ngOnInit() { 19 | this.settingsService.getSettings().subscribe((settings) => { 20 | this.dbName = settings.name || 'n/a'; 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/heroes/hero-detail/hero-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hero Details 4 | 5 | 6 |
7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/heroes/hero-detail/hero-detail.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .mat-card { 4 | @include mat-card-layout; 5 | @include editarea-margins; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/heroes/hero-detail/hero-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | ElementRef, 5 | EventEmitter, 6 | OnChanges, 7 | Output, 8 | ViewChild, 9 | SimpleChanges, 10 | ChangeDetectionStrategy 11 | } from '@angular/core'; 12 | 13 | import { Hero } from '../../core'; 14 | 15 | @Component({ 16 | selector: 'vk-hero-detail', 17 | templateUrl: './hero-detail.component.html', 18 | styleUrls: ['./hero-detail.component.scss'], 19 | changeDetection: ChangeDetectionStrategy.OnPush 20 | }) 21 | export class HeroDetailComponent implements OnChanges { 22 | @Input() hero: Hero; 23 | @Output() unselect = new EventEmitter(); 24 | @Output() add = new EventEmitter(); 25 | @Output() update = new EventEmitter(); 26 | @ViewChild('name', { static: true }) nameElement: ElementRef; 27 | 28 | addMode = false; 29 | 30 | editingHero: Hero; 31 | 32 | ngOnChanges(changes: SimpleChanges) { 33 | this.setFocus(); 34 | if (this.hero && this.hero.id) { 35 | this.editingHero = { ...this.hero }; 36 | this.addMode = false; 37 | } else { 38 | this.editingHero = { id: undefined, name: '', description: '' }; 39 | this.addMode = true; 40 | } 41 | } 42 | 43 | addHero() { 44 | this.add.emit(this.editingHero); 45 | this.clear(); 46 | } 47 | 48 | clear() { 49 | this.unselect.emit(); 50 | } 51 | 52 | saveHero() { 53 | if (this.addMode) { 54 | this.addHero(); 55 | } else { 56 | this.updateHero(); 57 | } 58 | } 59 | 60 | setFocus() { 61 | this.nameElement.nativeElement.focus(); 62 | } 63 | 64 | updateHero() { 65 | this.update.emit(this.editingHero); 66 | this.clear(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/heroes/hero-list/hero-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Heroes 4 | 5 | 6 |
    7 |
  • 8 | 12 |
    13 | 14 |
    15 |
    {{hero.name}}
    16 |
    {{hero.description}}
    17 |
    18 |
    19 |
  • 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app/heroes/hero-list/hero-list.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .heroes { 4 | @include item-list; 5 | } 6 | 7 | .mat-card { 8 | @include mat-card-layout; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/heroes/hero-list/hero-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, EventEmitter, Input, Output, ChangeDetectionStrategy, 3 | } from '@angular/core'; 4 | import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 5 | import { Hero, ModalComponent } from '../../core'; 6 | import { flyInOutAnimation, staggerListAnimation } from '../../core/animations'; 7 | 8 | @Component({ 9 | selector: 'vk-hero-list', 10 | templateUrl: './hero-list.component.html', 11 | styleUrls: ['./hero-list.component.scss'], 12 | animations: [flyInOutAnimation, staggerListAnimation], 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class HeroListComponent { 16 | @Input() heroes: Hero[]; 17 | @Input() selectedHero: Hero; 18 | @Output() deleted = new EventEmitter(); 19 | @Output() selected = new EventEmitter(); 20 | 21 | constructor(public dialog: MatDialog) {} 22 | 23 | byId(hero: Hero) { 24 | return hero.id; 25 | } 26 | 27 | onSelect(hero: Hero) { 28 | this.selected.emit(hero); 29 | } 30 | 31 | deleteHero(hero: Hero) { 32 | const dialogConfig = new MatDialogConfig(); 33 | dialogConfig.disableClose = true; 34 | dialogConfig.autoFocus = true; 35 | dialogConfig.width = '250px'; 36 | dialogConfig.data = { 37 | title: 'Delete Hero', 38 | message: `Do you want to delete ${hero.name}`, 39 | }; 40 | 41 | const dialogRef = this.dialog.open(ModalComponent, dialogConfig); 42 | 43 | dialogRef.afterClosed().subscribe((deleteIt) => { 44 | console.log('The dialog was closed'); 45 | if (deleteIt) { 46 | this.deleted.emit(hero); 47 | } 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/heroes/hero.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { throwError as observableThrowError } from 'rxjs'; 4 | import { catchError, tap } from 'rxjs/operators'; 5 | 6 | import { Hero, ToastService } from '../core'; 7 | // import { HeroesModule } from './heroes.module'; 8 | 9 | const api = '/api'; 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class HeroService { 13 | constructor(private http: HttpClient, private toastService: ToastService) {} 14 | 15 | logout() { 16 | return this.http.get(`${api}/logout`); 17 | } 18 | 19 | getProfile() { 20 | return this.http.get(`${api}/profile`); 21 | } 22 | 23 | getHeroes() { 24 | const url = `${api}/heroes`; 25 | const msg = 'Heroes retrieved successfully!'; 26 | return this.http 27 | .get(url) 28 | .pipe( 29 | tap(() => this.toastService.openSnackBar(msg, 'GET')), 30 | catchError(this.handleError) 31 | ); 32 | } 33 | 34 | private handleError(res: HttpErrorResponse) { 35 | console.error(res.error); 36 | return observableThrowError(res.error || 'Server error'); 37 | } 38 | 39 | deleteHero(hero: Hero) { 40 | return this.http 41 | .delete(`${api}/hero/${hero.id}`) 42 | .pipe( 43 | tap(() => 44 | this.toastService.openSnackBar(`Hero ${hero.name} deleted`, 'DELETE') 45 | ) 46 | ); 47 | } 48 | 49 | addHero(hero: Hero) { 50 | return this.http 51 | .post(`${api}/hero/`, hero) 52 | .pipe( 53 | tap(() => 54 | this.toastService.openSnackBar(`Hero ${hero.name} added`, 'POST') 55 | ) 56 | ); 57 | } 58 | 59 | updateHero(hero: Hero) { 60 | return this.http 61 | .put(`${api}/hero/${hero.id}`, hero) 62 | .pipe( 63 | tap(() => 64 | this.toastService.openSnackBar(`Hero ${hero.name} updated`, 'PUT') 65 | ) 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/heroes/heroes-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HeroesComponent } from './heroes/heroes.component'; 5 | 6 | const routes: Routes = [ 7 | { path: '', pathMatch: 'full', component: HeroesComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class HeroesRoutingModule {} 15 | -------------------------------------------------------------------------------- /src/app/heroes/heroes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { HeroesRoutingModule } from './heroes-routing.module'; 5 | import { HeroDetailComponent } from './hero-detail/hero-detail.component'; 6 | import { HeroesComponent } from './heroes/heroes.component'; 7 | import { HeroListComponent } from './hero-list/hero-list.component'; 8 | import { SharedModule } from '../shared/shared.module'; 9 | import { NgMaterialModule } from '../ng-material/ng-material.module'; 10 | 11 | @NgModule({ 12 | imports: [CommonModule, SharedModule, NgMaterialModule, HeroesRoutingModule], 13 | exports: [HeroesComponent, HeroDetailComponent], 14 | declarations: [HeroesComponent, HeroDetailComponent, HeroListComponent] 15 | }) 16 | export class HeroesModule { } 17 | -------------------------------------------------------------------------------- /src/app/heroes/heroes/heroes.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 12 | 22 |
23 |
24 |
25 |
26 |
27 | 31 | 37 |
38 |
39 |
44 | 50 | 51 |
52 |
53 | -------------------------------------------------------------------------------- /src/app/heroes/heroes/heroes.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .control-panel { 4 | @include control-panel-layout; 5 | } 6 | .content-container { 7 | @include content-container-layout; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/heroes/heroes/heroes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { finalize } from 'rxjs/operators'; 3 | 4 | import { Hero } from '../../core'; 5 | import { HeroService } from '../hero.service'; 6 | import { openCloseAnimation } from '../../core/animations'; 7 | 8 | @Component({ 9 | selector: 'vk-heroes', 10 | templateUrl: './heroes.component.html', 11 | styleUrls: ['./heroes.component.scss'], 12 | animations: [openCloseAnimation] 13 | }) 14 | export class HeroesComponent implements OnInit { 15 | addingHero = false; 16 | selectedHero: Hero; 17 | 18 | heroes: Hero[]; 19 | loading: boolean; 20 | 21 | constructor(private heroService: HeroService) {} 22 | 23 | ngOnInit() { 24 | this.getHeroes(); 25 | } 26 | 27 | clear() { 28 | this.addingHero = false; 29 | this.selectedHero = null; 30 | } 31 | 32 | deleteHero(hero: Hero) { 33 | this.loading = true; 34 | this.unselect(); 35 | this.heroService 36 | .deleteHero(hero) 37 | .pipe(finalize(() => (this.loading = false))) 38 | .subscribe( 39 | () => (this.heroes = this.heroes.filter(h => h.id !== hero.id)) 40 | ); 41 | } 42 | 43 | enableAddMode() { 44 | this.addingHero = true; 45 | this.selectedHero = null; 46 | } 47 | 48 | getHeroes() { 49 | this.loading = true; 50 | this.heroService 51 | .getHeroes() 52 | .pipe(finalize(() => (this.loading = false))) 53 | .subscribe(heroes => (this.heroes = heroes)); 54 | this.unselect(); 55 | } 56 | 57 | onSelect(hero: Hero) { 58 | this.addingHero = false; 59 | this.selectedHero = hero; 60 | } 61 | 62 | update(hero: Hero) { 63 | this.loading = true; 64 | this.heroService 65 | .updateHero(hero) 66 | .pipe(finalize(() => (this.loading = false))) 67 | .subscribe( 68 | () => 69 | (this.heroes = this.heroes.map(h => (h.id === hero.id ? hero : h))) 70 | ); 71 | } 72 | 73 | add(hero: Hero) { 74 | this.loading = true; 75 | this.heroService 76 | .addHero(hero) 77 | .pipe(finalize(() => (this.loading = false))) 78 | .subscribe(addedHero => (this.heroes = this.heroes.concat(addedHero))); 79 | } 80 | 81 | unselect() { 82 | this.addingHero = false; 83 | this.selectedHero = null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | 6 | 7 | Developing Locally 8 | 9 | Local Development 10 | 11 |

Angular app communicating to a Express.js API on Node.js.

12 |

Communicating to a Mongo DB through the Mongoose and Mongo DB APis.

13 |
14 |
15 |
16 |
17 | 19 | 20 | 21 | Cloud Ready Node.js and Database 22 | 23 | Local Development 24 | 25 |

Cloud based dockerized solution.

26 |

Angular app communicating to a Express.js API on Node.js.

27 |

Communicating to a Cosmos DB through the Cosmos SDK

28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .content-container { 4 | margin: 4em; 5 | } 6 | 7 | .control-panel { 8 | @include control-panel-layout; 9 | } 10 | .content-container { 11 | @include content-container-layout; 12 | } 13 | 14 | .mat-card { 15 | @include mat-card-layout; 16 | 17 | .card-image { 18 | width: 365px; 19 | max-width: 365px; 20 | max-height: 470px; 21 | margin-left: 0px; 22 | } 23 | mat-card-content { 24 | min-height: 110px; 25 | } 26 | } 27 | 28 | button.mat-raised-button { 29 | margin-left: 16px; 30 | margin-bottom: 20px; 31 | width: 200px; 32 | } 33 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { openCloseAnimation } from '../core/animations'; 3 | 4 | @Component({ 5 | selector: 'vk-home', 6 | templateUrl: './home.component.html', 7 | styleUrls: ['./home.component.scss'], 8 | animations: [openCloseAnimation], 9 | }) 10 | export class HomeComponent implements OnInit { 11 | showLocal = false; 12 | 13 | showCloudReady = false; 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/ng-material/ng-material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatDialogModule } from '@angular/material/dialog'; 5 | import { MatIconModule } from '@angular/material/icon'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 8 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 9 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 10 | import { MatToolbarModule } from '@angular/material/toolbar'; 11 | import { MatTooltipModule } from '@angular/material/tooltip'; 12 | 13 | const myModules = [ 14 | MatButtonModule, 15 | MatCardModule, 16 | MatDialogModule, 17 | MatIconModule, 18 | MatInputModule, 19 | MatProgressSpinnerModule, 20 | MatSlideToggleModule, 21 | MatSnackBarModule, 22 | MatToolbarModule, 23 | MatTooltipModule 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [myModules], 28 | exports: [myModules], 29 | declarations: [] 30 | }) 31 | export class NgMaterialModule {} 32 | -------------------------------------------------------------------------------- /src/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { HomeComponent } from './home/home.component'; 3 | 4 | export const routes: Routes = [ 5 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 6 | { 7 | path: 'home', 8 | component: HomeComponent, 9 | data: { animation: 'HomePage' } 10 | }, 11 | { 12 | path: 'heroes', 13 | loadChildren: () => 14 | import('./heroes/heroes.module').then(m => m.HeroesModule), 15 | data: { animation: 'HeroesPage' } 16 | }, 17 | { 18 | path: 'villains', 19 | loadChildren: () => 20 | import('./villains/villains.module').then(m => m.VillainsModule) 21 | // data: { animation: 'VillainsPage' } 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | @NgModule({ 6 | imports: [CommonModule, FormsModule, ReactiveFormsModule], 7 | exports: [FormsModule, ReactiveFormsModule], 8 | declarations: [], 9 | }) 10 | export class SharedModule {} 11 | -------------------------------------------------------------------------------- /src/app/villains/villain-detail/villain-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Villain Details 4 | 5 | 6 |
7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/villains/villain-detail/villain-detail.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .mat-card { 4 | @include mat-card-layout; 5 | @include editarea-margins; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/villains/villain-detail/villain-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | ElementRef, 5 | EventEmitter, 6 | OnChanges, 7 | Output, 8 | ViewChild, 9 | SimpleChanges, 10 | ChangeDetectionStrategy 11 | } from '@angular/core'; 12 | 13 | import { Villain } from '../../core'; 14 | import { Validators, FormBuilder, FormGroup } from '@angular/forms'; 15 | 16 | @Component({ 17 | selector: 'vk-villain-detail', 18 | templateUrl: './villain-detail.component.html', 19 | styleUrls: ['./villain-detail.component.scss'], 20 | changeDetection: ChangeDetectionStrategy.OnPush 21 | }) 22 | export class VillainDetailComponent implements OnChanges { 23 | @Input() villain: Villain; 24 | @Output() unselect = new EventEmitter(); 25 | @Output() add = new EventEmitter(); 26 | @Output() update = new EventEmitter(); 27 | 28 | @ViewChild('name', { static: true }) nameElement: ElementRef; 29 | 30 | addMode = false; 31 | 32 | form = this.fb.group({ 33 | id: [], 34 | name: ['', Validators.required], 35 | description: [''] 36 | }); 37 | 38 | constructor(private fb: FormBuilder) {} 39 | 40 | ngOnChanges(changes: SimpleChanges) { 41 | this.setFocus(); 42 | if (this.villain && this.villain.id) { 43 | this.form.patchValue(this.villain); 44 | this.addMode = false; 45 | } else { 46 | this.form.reset(); 47 | this.addMode = true; 48 | } 49 | } 50 | 51 | addVillain(form: FormGroup) { 52 | const { value, valid, touched } = form; 53 | if (touched && valid) { 54 | this.add.emit({ ...this.villain, ...value }); 55 | } 56 | this.clear(); 57 | } 58 | 59 | clear() { 60 | this.unselect.emit(); 61 | } 62 | 63 | saveVillain(form: FormGroup) { 64 | if (this.addMode) { 65 | this.addVillain(form); 66 | } else { 67 | this.updateVillain(form); 68 | } 69 | } 70 | 71 | setFocus() { 72 | this.nameElement.nativeElement.focus(); 73 | } 74 | 75 | updateVillain(form: FormGroup) { 76 | const { value, valid, touched } = form; 77 | if (touched && valid) { 78 | this.update.emit({ ...this.villain, ...value }); 79 | } 80 | this.clear(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/villains/villain-list/villain-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Villains 4 | 5 | 6 |
    7 |
  • 8 | 12 |
    13 | 14 |
    15 |
    {{villain.name}}
    16 |
    {{villain.description}}
    17 |
    18 |
    19 |
  • 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app/villains/villain-list/villain-list.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .villains { 4 | @include item-list; 5 | } 6 | 7 | .mat-card { 8 | @include mat-card-layout; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/villains/villain-list/villain-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, EventEmitter, Input, Output, ChangeDetectionStrategy, 3 | } from '@angular/core'; 4 | import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 5 | import { ModalComponent, Villain } from '../../core'; 6 | import { flyInOutAnimation, staggerListAnimation } from '../../core/animations'; 7 | 8 | @Component({ 9 | selector: 'vk-villain-list', 10 | templateUrl: './villain-list.component.html', 11 | styleUrls: ['./villain-list.component.scss'], 12 | animations: [flyInOutAnimation, staggerListAnimation], 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class VillainListComponent { 16 | @Input() villains: Villain[]; 17 | @Input() selectedVillain: Villain; 18 | @Output() deleted = new EventEmitter(); 19 | @Output() selected = new EventEmitter(); 20 | 21 | constructor(public dialog: MatDialog) {} 22 | 23 | byId(villain: Villain) { 24 | return villain.id; 25 | } 26 | 27 | onSelect(villain: Villain) { 28 | this.selected.emit(villain); 29 | } 30 | 31 | deleteVillain(villain: Villain) { 32 | const dialogConfig = new MatDialogConfig(); 33 | dialogConfig.disableClose = true; 34 | dialogConfig.autoFocus = true; 35 | dialogConfig.width = '250px'; 36 | dialogConfig.data = { 37 | title: 'Delete Villain', 38 | message: `Do you want to delete ${villain.name}`, 39 | }; 40 | 41 | const dialogRef = this.dialog.open(ModalComponent, dialogConfig); 42 | 43 | dialogRef.afterClosed().subscribe((deleteIt) => { 44 | console.log('The dialog was closed'); 45 | if (deleteIt) { 46 | this.deleted.emit(villain); 47 | } 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/villains/villain.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { throwError as observableThrowError } from 'rxjs'; 4 | import { catchError, map, tap } from 'rxjs/operators'; 5 | 6 | import { ToastService, Villain } from '../core'; 7 | 8 | const api = '/api'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class VillainService { 12 | constructor(private http: HttpClient, private toastService: ToastService) {} 13 | 14 | logout() { 15 | return this.http.get(`${api}/logout`); 16 | } 17 | 18 | getProfile() { 19 | return this.http.get(`${api}/profile`); 20 | } 21 | 22 | getVillain(id: number) { 23 | return this.http 24 | .get>(`${api}/villains/${id}`) 25 | .pipe( 26 | map(villain => villain), 27 | tap(() => 28 | this.toastService.openSnackBar( 29 | 'Villain retrieved successfully!', 30 | 'GET' 31 | ) 32 | ), 33 | catchError(this.handleError) 34 | ); 35 | } 36 | 37 | getVillains() { 38 | return this.http 39 | .get>(`${api}/villains`) 40 | .pipe( 41 | map(villains => villains), 42 | tap(() => 43 | this.toastService.openSnackBar( 44 | 'Villains retrieved successfully!', 45 | 'GET' 46 | ) 47 | ), 48 | catchError(this.handleError) 49 | ); 50 | } 51 | 52 | private handleError(res: HttpErrorResponse) { 53 | console.error(res.error); 54 | return observableThrowError(res.error || 'Server error'); 55 | } 56 | 57 | deleteVillain(villain: Villain) { 58 | return this.http 59 | .delete(`${api}/villain/${villain.id}`) 60 | .pipe( 61 | tap(() => 62 | this.toastService.openSnackBar( 63 | `Villain ${villain.name} deleted`, 64 | 'DELETE' 65 | ) 66 | ) 67 | ); 68 | } 69 | 70 | addVillain(villain: Villain) { 71 | return this.http 72 | .post(`${api}/villain/`, villain) 73 | .pipe( 74 | tap(() => 75 | this.toastService.openSnackBar( 76 | `Villain ${villain.name} added`, 77 | 'POST' 78 | ) 79 | ) 80 | ); 81 | } 82 | 83 | updateVillain(villain: Villain) { 84 | return this.http 85 | .put(`${api}/villain/${villain.id}`, villain) 86 | .pipe( 87 | tap(() => 88 | this.toastService.openSnackBar( 89 | `Villain ${villain.name} updated`, 90 | 'PUT' 91 | ) 92 | ) 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/villains/villains-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { VillainsComponent } from './villains/villains.component'; 5 | 6 | const routes: Routes = [ 7 | { path: '', pathMatch: 'full', component: VillainsComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class VillainsRoutingModule {} 15 | -------------------------------------------------------------------------------- /src/app/villains/villains.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { NgMaterialModule } from '../ng-material/ng-material.module'; 4 | import { SharedModule } from '../shared/shared.module'; 5 | import { VillainDetailComponent } from './villain-detail/villain-detail.component'; 6 | import { VillainListComponent } from './villain-list/villain-list.component'; 7 | import { VillainsRoutingModule } from './villains-routing.module'; 8 | import { VillainsComponent } from './villains/villains.component'; 9 | 10 | @NgModule({ 11 | imports: [CommonModule, SharedModule, NgMaterialModule,VillainsRoutingModule], 12 | exports: [VillainsComponent, VillainDetailComponent], 13 | declarations: [ 14 | VillainsComponent, 15 | VillainDetailComponent, 16 | VillainListComponent 17 | ] 18 | }) 19 | export class VillainsModule {} 20 | -------------------------------------------------------------------------------- /src/app/villains/villains/villains.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 6 |
7 |
8 |
9 |
10 |
11 | 15 | 17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/villains/villains/villains.component.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | .control-panel { 4 | @include control-panel-layout; 5 | } 6 | .content-container { 7 | @include content-container-layout; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/villains/villains/villains.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { finalize } from 'rxjs/operators'; 3 | import { Villain } from '../../core'; 4 | import { openCloseAnimation } from '../../core/animations'; 5 | import { VillainService } from '../villain.service'; 6 | 7 | @Component({ 8 | selector: 'vk-villains', 9 | templateUrl: './villains.component.html', 10 | styleUrls: ['./villains.component.scss'], 11 | animations: [openCloseAnimation], 12 | }) 13 | export class VillainsComponent implements OnInit { 14 | addingVillain = false; 15 | 16 | selectedVillain: Villain; 17 | 18 | villains: Villain[]; 19 | 20 | loading: boolean; 21 | 22 | constructor(private villainService: VillainService) {} 23 | 24 | ngOnInit() { 25 | this.getVillains(); 26 | } 27 | 28 | clear() { 29 | this.addingVillain = false; 30 | this.selectedVillain = null; 31 | } 32 | 33 | deleteVillain(villain: Villain) { 34 | this.loading = true; 35 | this.unselect(); 36 | this.villainService 37 | .deleteVillain(villain) 38 | .pipe(finalize(() => (this.loading = false))) 39 | .subscribe(() => (this.villains = this.villains.filter(h => h.id !== villain.id))); 40 | } 41 | 42 | enableAddMode() { 43 | this.addingVillain = true; 44 | this.selectedVillain = null; 45 | } 46 | 47 | getVillains() { 48 | this.loading = true; 49 | this.villainService 50 | .getVillains() 51 | .pipe(finalize(() => (this.loading = false))) 52 | .subscribe(villains => (this.villains = villains)); 53 | this.unselect(); 54 | } 55 | 56 | onSelect(villain: Villain) { 57 | this.addingVillain = false; 58 | this.selectedVillain = villain; 59 | } 60 | 61 | update(villain: Villain) { 62 | this.loading = true; 63 | this.villainService 64 | .updateVillain(villain) 65 | .pipe(finalize(() => (this.loading = false))) 66 | .subscribe( 67 | () => (this.villains = this.villains.map(h => (h.id === villain.id ? villain : h))), 68 | ); 69 | } 70 | 71 | add(villain: Villain) { 72 | this.loading = true; 73 | this.villainService 74 | .addVillain(villain) 75 | .pipe(finalize(() => (this.loading = false))) 76 | .subscribe(addedvillain => (this.villains = this.villains.concat(addedvillain))); 77 | } 78 | 79 | unselect() { 80 | this.addingVillain = false; 81 | this.selectedVillain = null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/azureappservice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/azureappservice.png -------------------------------------------------------------------------------- /src/assets/ignite-cloud.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/ignite-cloud.jpg -------------------------------------------------------------------------------- /src/assets/ignite-local.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/ignite-local.jpg -------------------------------------------------------------------------------- /src/assets/jp-48.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/jp-48.jpg -------------------------------------------------------------------------------- /src/assets/jp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/jp.jpg -------------------------------------------------------------------------------- /src/assets/ng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/ng.png -------------------------------------------------------------------------------- /src/assets/ngvikings-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/ngvikings-logo.png -------------------------------------------------------------------------------- /src/assets/wb-48.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/wb-48.jpg -------------------------------------------------------------------------------- /src/assets/wb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/assets/wb.jpg -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/vikings/1717b7016cdf89a9433c5f3bf51ef73624a027bf/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vikings 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import 'hammerjs'; 4 | import { environment } from './environments/environment'; 5 | import { AppModule } from './app/app.module'; 6 | import { AppDevModule } from './app/app-dev.module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | // If you want to pretend there is a database 13 | platformBrowserDynamic().bootstrapModule(AppDevModule); 14 | 15 | // If you have a real database, use this 16 | // platformBrowserDynamic().bootstrapModule(AppModule); 17 | -------------------------------------------------------------------------------- /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 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @import 'theme'; 2 | 3 | @mixin primary-color { 4 | $primary-color: map-get($primary, 800); 5 | color: $primary-color; 6 | } 7 | @mixin primary-background-contrast-color { 8 | $primary-color: map-get($primary, 400); 9 | $font-color: map-get(map-get($primary, contrast), 400); 10 | background-color: $primary-color; 11 | color: $font-color !important; 12 | } 13 | @mixin accent-color { 14 | $accent-font-color: map-get($accent, 800); 15 | color: $accent-font-color; 16 | } 17 | @mixin accent-background-color { 18 | $accent-color: map-get($accent, 700); 19 | $accent-font-color: map-get(map-get($accent, contrast), 700); 20 | background-color: $accent-color; 21 | color: $accent-font-color !important; 22 | } 23 | @mixin selected-style { 24 | $background-color: map-get($accent, 800); 25 | $font-color: map-get(map-get($accent, contrast), 800); 26 | background: $background-color !important; 27 | color: $font-color !important; 28 | } 29 | @mixin hover-style { 30 | $background-color: map-get($accent, 400); 31 | $font-color: map-get(map-get($accent, contrast), 900); 32 | background: $background-color !important; 33 | color: $font-color !important; 34 | } 35 | @mixin selected-hover-style { 36 | $background-color: map-get($accent, 600); 37 | $font-color: map-get(map-get($accent, contrast), 600); 38 | background: $background-color !important; 39 | color: $font-color !important; 40 | } 41 | 42 | @mixin item-list { 43 | margin: 0 0 2em 0; 44 | list-style-type: none; 45 | padding: 0; 46 | width: 25em; 47 | .selected, 48 | .selected > .item-text * { 49 | background-color: rgb(0, 120, 215) !important; 50 | color: white; 51 | } 52 | li { 53 | cursor: pointer; 54 | position: relative; 55 | margin: 0.5em; 56 | height: 4em; 57 | } 58 | .description { 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | width: 15em; 62 | white-space: nowrap; 63 | margin: 5px 0px; 64 | } 65 | .name { 66 | @include primary-color; 67 | } 68 | .item-container { 69 | display: flex; 70 | flex-flow: row wrap; 71 | } 72 | > * { 73 | flex: 1 100%; 74 | } 75 | .selectable-item { 76 | display: flex; 77 | flex-flow: row wrap; 78 | flex: 18 auto; 79 | order: 0; 80 | padding: 0; 81 | margin: 0; 82 | border-top: #85b7de 0.5pt solid; 83 | &:hover { 84 | color: #607d8b; 85 | color: rgb(0, 120, 215); 86 | background-color: #ddd; 87 | left: 1px; 88 | } 89 | &.selected:hover { 90 | color: white; 91 | } 92 | } 93 | .item-text { 94 | flex: 1 auto; 95 | order: 2; 96 | padding: 10px; 97 | } 98 | .badge { 99 | flex: 1 auto; 100 | order: 1; 101 | font-size: small; 102 | color: #ffffff; 103 | padding: 1.5em 1em 0em 1em; 104 | background-color: rgb(134, 183, 221); 105 | margin: 0em 0em 0em 0em; 106 | max-width: 1.5em; 107 | text-align: center; 108 | } 109 | button.delete-button { 110 | margin-right: 12px; 111 | margin-top: 6px; 112 | order: 1; 113 | } 114 | } 115 | 116 | @mixin mat-card-layout { 117 | width: 366px; 118 | margin: 1em; 119 | padding: 0px; 120 | .mat-card-header { 121 | background-color: #85b7de; 122 | padding: 18px 12px 6px 24px; 123 | color: #ffffff; 124 | } 125 | .mat-card-content { 126 | padding: 24px; 127 | } 128 | } 129 | 130 | @mixin editarea-margins { 131 | button { 132 | margin: 0.5em 1em; 133 | } 134 | .editfields { 135 | margin: 1em; 136 | .mat-form-field { 137 | width: 100%; 138 | } 139 | } 140 | } 141 | 142 | @mixin control-panel-layout { 143 | margin: 8px; 144 | margin-left: 1em; 145 | display: flex; 146 | flex-flow: column wrap; 147 | > * { 148 | flex: 1 100%; 149 | } 150 | .button-panel { 151 | flex: 1 auto; 152 | margin: 8px 0px; 153 | order: 1; 154 | } 155 | button.mat-raised-button.mat-primary { 156 | margin-right: 8px; 157 | } 158 | .filter-panel { 159 | flex: 1 auto; 160 | margin: 8px 0px; 161 | order: 2; 162 | } 163 | } 164 | 165 | @mixin content-container-layout { 166 | display: flex; 167 | flex-flow: row wrap; 168 | > * { 169 | flex: 1 100%; 170 | } 171 | .list-container { 172 | flex: 1 auto; 173 | order: 1; 174 | max-width: 30em; 175 | } 176 | .detail-container { 177 | flex: 1 auto; 178 | order: 2; 179 | max-width: 22em; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin'; 2 | 3 | * { 4 | font-family: Arial; 5 | } 6 | h2 { 7 | color: #444; 8 | font-weight: lighter; 9 | } 10 | html, 11 | body { 12 | margin: 0px; 13 | font-family: Arial; 14 | } 15 | 16 | .bg { 17 | -webkit-background-size: cover; 18 | -moz-background-size: cover; 19 | -o-background-size: cover; 20 | background-size: cover; 21 | -webkit-background-size: cover; 22 | -moz-background-size: cover; 23 | -o-background-size: cover; 24 | background-size: cover; 25 | min-width: 1280px; 26 | min-height: 1080px; 27 | margin-top: -60px; 28 | width: 100%; 29 | height: auto; 30 | opacity: 0.4; 31 | margin-top: 106px; 32 | position: absolute; 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | // https://material.angular.io/guide/theming 2 | // @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | // @import '~@angular/material/prebuilt-themes/indigo-pink.css'; 4 | @import '~@angular/material/theming'; 5 | @include mat-core(); 6 | 7 | $primary: mat-palette($mat-blue, 800); 8 | $accent: mat-palette($mat-pink, A200, A100, A400); 9 | $warn: mat-palette($mat-deep-orange); 10 | $light-theme: mat-light-theme($primary, $accent, $warn); 11 | @include angular-material-theme($light-theme); 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /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.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 | "vk", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "vk", 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-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true 87 | }, 88 | "rulesDirectory": [ 89 | "codelyzer" 90 | ] 91 | } 92 | --------------------------------------------------------------------------------