├── .editorconfig ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── base.html ├── config.js ├── docs └── index.html ├── gulpfile.js ├── index.html ├── package.json ├── production.html ├── server ├── .env.example ├── bootstrap.js ├── config │ ├── base.js │ ├── dev.js │ ├── index.js │ └── prod.js ├── controller │ └── spotify.js ├── index.js ├── middleware │ ├── httpsRedirect.js │ └── isFile.js ├── newrelic.js └── routes │ ├── api.js │ └── routes.js ├── src ├── assets │ ├── .gitkeep │ ├── favicon.ico │ └── mock │ │ ├── playlist.json │ │ └── questions.json ├── css │ ├── .gitkeep │ ├── app.less │ ├── styles │ │ ├── fonts.less │ │ ├── index.less │ │ ├── loading-animation.less │ │ ├── main.less │ │ ├── material-icons.less │ │ └── materialize-normalize.less │ ├── variables │ │ └── index.less │ └── vendor │ │ └── index.less ├── js │ ├── bootstrap.ts │ ├── components │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.html │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── landing │ │ │ ├── index.ts │ │ │ ├── landing.component.ts │ │ │ ├── landing.css │ │ │ ├── landing.html │ │ │ └── region_options.ts │ │ ├── quiz │ │ │ ├── answers │ │ │ │ ├── answers.ts │ │ │ │ ├── choice-album │ │ │ │ │ ├── answer-choice-album.component.ts │ │ │ │ │ ├── answer-choice-album.css │ │ │ │ │ ├── answer-choice-album.html │ │ │ │ │ └── index.ts │ │ │ │ ├── choice-artist │ │ │ │ │ ├── answer-choice-artist.component.ts │ │ │ │ │ ├── answer-choice-artist.css │ │ │ │ │ ├── answer-choice-artist.html │ │ │ │ │ └── index.ts │ │ │ │ ├── choice-track │ │ │ │ │ ├── answer-choice-track.component.ts │ │ │ │ │ ├── answer-choice-track.css │ │ │ │ │ ├── answer-choice-track.html │ │ │ │ │ └── index.ts │ │ │ │ ├── common.css │ │ │ │ ├── generic_answer.ts │ │ │ │ └── index.ts │ │ │ ├── done │ │ │ │ ├── index.ts │ │ │ │ ├── quiz-done.component.ts │ │ │ │ ├── quiz-done.css │ │ │ │ └── quiz-done.html │ │ │ ├── index.ts │ │ │ ├── nav │ │ │ │ ├── index.ts │ │ │ │ ├── quiz-nav.component.ts │ │ │ │ ├── quiz-nav.css │ │ │ │ └── quiz-nav.html │ │ │ ├── questions │ │ │ │ ├── album │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── question-album.component.ts │ │ │ │ │ ├── question-album.css │ │ │ │ │ └── question-album.html │ │ │ │ ├── audio │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── question-audio.component.ts │ │ │ │ │ ├── question-audio.css │ │ │ │ │ └── question-audio.html │ │ │ │ ├── common.css │ │ │ │ ├── generic_question.ts │ │ │ │ ├── image │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── question-image.component.ts │ │ │ │ │ ├── question-image.css │ │ │ │ │ └── question-image.html │ │ │ │ ├── index.ts │ │ │ │ ├── question.component.ts │ │ │ │ ├── question.css │ │ │ │ ├── question.html │ │ │ │ ├── questions.ts │ │ │ │ ├── track │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── question-track.component.ts │ │ │ │ │ ├── question-track.css │ │ │ │ │ └── question-track.html │ │ │ │ └── types.ts │ │ │ ├── quiz.component.ts │ │ │ ├── quiz.css │ │ │ ├── quiz.html │ │ │ └── status │ │ │ │ ├── index.ts │ │ │ │ ├── quiz-status.component.ts │ │ │ │ ├── quiz-status.css │ │ │ │ └── quiz-status.html │ │ └── unsubscriber.ts │ ├── contracts │ │ ├── album.ts │ │ ├── artist.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── item.ts │ │ ├── playlist.ts │ │ ├── question.ts │ │ ├── question_type.ts │ │ └── track.ts │ ├── directives │ │ ├── container-ref.directive.ts │ │ └── index.ts │ ├── facades │ │ ├── error_handler.ts │ │ └── index.ts │ ├── helpers │ │ ├── common.ts │ │ ├── index.ts │ │ ├── scroll.ts │ │ └── tracks.ts │ ├── main.dev.ts │ ├── main.prod.ts │ ├── modules │ │ ├── app.module.ts │ │ └── index.ts │ ├── routes │ │ ├── app.routes.ts │ │ └── index.ts │ └── services │ │ ├── index.ts │ │ ├── locale.service.ts │ │ ├── playlist.service.ts │ │ └── quiz.service.ts └── types │ ├── import.d.ts │ └── iscroll.d.ts ├── system.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in 2 | # this file, please see the EditorConfig documentation: 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [{.travis.yml,package.json}] 19 | # The indent size used in the `package.json` file cannot be changed 20 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 21 | indent_size = 2 22 | indent_style = space 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | bower_components 4 | dist 5 | temp 6 | npm-debug.log 7 | server/.env 8 | typings 9 | .idea 10 | newrelic_agent.log 11 | pathmap.json 12 | .DS_Store 13 | tsconfig.json.backup 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Fabian Pirklbauer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Music Quiz 2 | 3 | # Quickstart 4 | 5 | ```sh 6 | $ git clone https://github.com/fabiandev/angular-quiz-app.git 7 | $ cd angular-quiz-app 8 | $ npm install 9 | $ npm start 10 | ``` 11 | 12 | > Tip: You can use [yarn](https://yarnpkg.com/) instead of [npm](https://npmjs.com). 13 | 14 | # Live Example 15 | 16 | See this project in action: 17 | 18 | [https://fabiandev.io/angular-quiz-app/](https://fabiandev.io/angular-quiz-app/) 19 | 20 | # Credits 21 | 22 | This app uses: 23 | - [Angular](https://angular.io) as a front-end framework. 24 | - [Express](https://github.com/expressjs/express) with [spotify-web-api-node](https://github.com/thelinmichael/spotify-web-api-node) for the server. 25 | - [Spotify API](https://developer.spotify.com/web-api/) for the quiz data. 26 | - [css-animator](https://github.com/fabiandev/css-animator) and [animate.css](https://daneden.github.io/animate.css/) for animations. 27 | - [Materialize](http://materializecss.com/) for styling. 28 | - [Material icons](https://github.com/google/material-design-icons/) and [Icons8 Flat Color Icons](https://github.com/icons8/flat-color-icons) for icons. 29 | - [iScroll](https://github.com/cubiq/iscroll) for a better mobile scrolling experience. 30 | 31 | # Documentation 32 | 33 | ## CLI 34 | 35 | ### Dependencies 36 | 37 | We use npm or yarn and jspm (currently `jspm@beta`) to install dependencies. 38 | For simplicity [gulp](http://gulpjs.com) and [jspm](http://jspm.io) can be installed globally, by using the `-g` flag. 39 | 40 | ```sh 41 | $ npm install -g gulp jspm@beta 42 | ``` 43 | 44 | > Make sure that you have [Node.js](https://nodejs.org/) installed, [npm](https://npmjs.com) comes 45 | > with it. You can check with `node --version`. For faster npm dependency installs, use [yarn](https://yarnpkg.com/). 46 | 47 | To install **development dependencies**, used e.g. in gulp tasks use: 48 | 49 | ```sh 50 | $ npm install --save-dev module-name 51 | ``` 52 | 53 | > or `yarn add module-name --dev` 54 | 55 | To install **application dependencies**, used on the server side use: 56 | 57 | ```sh 58 | $ npm install --save module-name 59 | ``` 60 | 61 | > or `yarn add module-name` 62 | 63 | To install **client side dependencies**, use jspm: 64 | 65 | ```sh 66 | $ jspm install modulename && npm run update-paths 67 | ``` 68 | 69 | The execution of `update-paths` is required to have all jspm package also mapped 70 | in `compilerOptions.paths` of `tsconfig.json`. 71 | 72 | > jspm also supports `install npm:modulename` and `install github:user/repo` 73 | 74 | ### Typings 75 | 76 | Typings are used to tell the [TypeScript](https://www.typescriptlang.org) 77 | compiler about definitions. You can install them via npm just like this: 78 | 79 | ```sh 80 | $ npm install @types/core-js 81 | ``` 82 | 83 | ### Building 84 | 85 | #### Production Build 86 | 87 | The production build should be used to compile the app for **deployment**. 88 | It will do it's best to keep the target files as small as possible. 89 | 90 | ```sh 91 | $ gulp build 92 | ``` 93 | 94 | #### Development Build 95 | 96 | A development build performs similar tasks as a production build, but makes debugging a lot easier. 97 | 98 | ```sh 99 | $ gulp dev-build 100 | ``` 101 | 102 | #### Watch Changes 103 | 104 | During development make use of the watch task, which does not need to compile the entire app on each change. 105 | The application will be transpiled on demand in the browser. 106 | 107 | ```sh 108 | $ gulp watch 109 | ``` 110 | 111 | > You may also execute `gulp watch-build` to perform those actions only once. 112 | 113 | ### Local Server 114 | 115 | Before starting the server copy `.env.example` in `/server` and name it `.env`, get 116 | [Spotify API](https://developer.spotify.com/my-applications/#!/) keys and fill them in. 117 | 118 | > NEVER PASTE YOUR KEYS IN THE EXAMPLE FILE OR ANYWHERE ELSE! 119 | 120 | To **start the sever** type: 121 | 122 | ```sh 123 | $ npm start 124 | ``` 125 | 126 | > or `yarn start` 127 | 128 | > The server will be started with the `dist` directory as root, and a built version 129 | > of the app will be used. Make sure to run `gulp build` or `gul dev-build` first. 130 | 131 | To **start a development server** type: 132 | 133 | ```sh 134 | $ npm start dev 135 | ``` 136 | 137 | > or `yarn start dev` 138 | 139 | > The server will be started on the very top level of the application code. 140 | > All files (including dependencies) are transpiled on-demand in the browser. 141 | > While developing, make sure `gulp watch` is running, to pick up index.html and 142 | > less-files changes. 143 | 144 | ### Deployment 145 | 146 | This app supports deployment on Heroku: 147 | 148 | ```sh 149 | $ git push heroku master 150 | ``` 151 | 152 | Just make sure to set the correct Node and npm environment variables: 153 | 154 | ```sh 155 | NODE_ENV=production 156 | NPM_CONFIG_PRODUCTION=false 157 | ``` 158 | 159 | > The npm production flag must be set to false that we can build the app on Heroku after pushing the repository. 160 | 161 | And of course you have to add `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` as environment variables. 162 | 163 | Optionally you may also add `NEW_RELIC_LICENSE_KEY` to enable monitoring by [New Relic](https://newrelic.com/). 164 | If you do not provide a license key, New Relic simply won't be enabled. 165 | 166 | > Tip: If you deploy to Heroku, you can add the [New Relic Add-on](https://elements.heroku.com/addons/newrelic) for free. 167 | 168 | ## Configuration 169 | 170 | This section covers how to configure the build tasks, the server and 171 | the application itself. 172 | 173 | ### Build Configuration - `config.js` 174 | 175 | You can set some configuration for TypeScript in `tsconfig.json` and in 176 | `tslint.json`. All other configuration can be found in `config.js`. 177 | 178 | Please take a closer look at the `config.js` file comment's on the configuration 179 | properties for more detailed explanations. 180 | 181 | #### config.src 182 | 183 | Type: `String` 184 | 185 | The folder, where the source files can be found, e.g. `./src` (no trailing slash!). 186 | 187 | #### config.dist 188 | 189 | Type: `String` 190 | 191 | The folder, where the built app will go to. Again, do not use a trailing slash. 192 | 193 | > *dist* is short for *distribution*. 194 | 195 | #### config.watch 196 | 197 | Type: `String|Array` 198 | 199 | Define which files should or shouldn't be watched, when using `gulp watch`. 200 | You can use the [globbing pattern](https://www.npmjs.com/package/minimatch) here. 201 | 202 | #### config.jspm 203 | 204 | Type: `Object` 205 | 206 | This configuration holds the command, that will be executed later via gulp when building the application. 207 | You can type `jspm` in the command line to see all available options. 208 | 209 | jspm internally uses the [SystemJS](https://github.com/systemjs/systemjs) [builder](https://github.com/systemjs/builder). 210 | 211 | #### config.less 212 | 213 | Type: `Object` 214 | 215 | Configure the [less](http://lesscss.org) gulp task, to create CSS files 216 | from LESS files. 217 | 218 | #### config.tslint 219 | 220 | Type: `Object` 221 | 222 | Define a globbing pattern, which TypeScript files to lint for errors. 223 | 224 | #### config.index 225 | 226 | Type: `String` 227 | 228 | Define the index file for the application. 229 | 230 | #### config.assets 231 | 232 | Type: `Object` 233 | 234 | Files to copy without further processing. 235 | 236 | 237 | #### config.copy 238 | 239 | Type: `Array` 240 | 241 | Files to copy into a desired location, but only preserve the path from the set `base`. 242 | 243 | ### Server Configuration - `server/index.js` 244 | 245 | You can set environment variables in `server/.env` (not included in this repo). 246 | Copy `server/.env.example` and rename it to `.env`. 247 | 248 | Other options are set in `server/config/app.js` for a production server, or 249 | `/server.config/app.dev.js` for a development server. 250 | 251 | ### Application Configuration 252 | 253 | Note, that the index.html is **not** inside the `src`, but on the very top level 254 | of the application code. 255 | 256 | > The `index.html` is processed by 257 | > [gulp-preprocess](https://github.com/jas/gulp-preprocess). 258 | 259 | For the dev server or a dev build, `src/js/main.dev.ts` 260 | will be used. For a production build, `src/js/main.prod.ts` is the entry point of the app. 261 | 262 | ## Extending 263 | 264 | It is possible to add questions and answers to this app, by performing a few steps discussed by examples below. 265 | 266 | ### Answers 267 | 268 | To add a custom answer `yesno`, create a directory `yesno` in `src/js/components/quiz/answers`, containing the following files: 269 | 270 | - `answer-yesno.component.ts` 271 | - `answer-yesno.html` 272 | - `answer-yesno.css` (optional) 273 | - `index.ts` 274 | 275 | ```ts 276 | // answer-yesno.component.ts 277 | 278 | import { Component } from '@angular/core'; 279 | import { GenericAnswer } from 'app/components'; 280 | 281 | import template from './answer-yesno.html'; 282 | import mainStyle from './answer-yesno.css'; 283 | import commonStyle from '../common.css'; 284 | 285 | @Component({ 286 | selector: 'answer-yesno', 287 | template: template, 288 | styles: [ 289 | commonStyle, 290 | mainStyle 291 | ] 292 | }) 293 | export class AnswerYesNoComponent extends GenericAnswer { 294 | 295 | protected init(): void { 296 | 297 | } 298 | 299 | } 300 | ``` 301 | 302 | ```html 303 | 304 | 305 |
306 |
307 | 308 | 309 |
310 |
311 | 312 | 313 |
314 |
315 | ``` 316 | 317 | ```ts 318 | // index.ts 319 | export * from './answer-yesno.component'; 320 | ``` 321 | 322 | Finally add an export to `src/js/components/quiz/answers/answers.ts`: 323 | 324 | ```ts 325 | export * from './yesno/index'; 326 | ``` 327 | 328 | ### Questions 329 | 330 | To add a new question type `simple`, define it in `src/js/components/quiz/questions/types.ts`: 331 | 332 | ```ts 333 | export enum QuestionType { 334 | // ... 335 | Simple, 336 | // ... 337 | } 338 | ``` 339 | 340 | Also create a directory `simple` in `src/js/components/quiz/questions`, containing the following files: 341 | 342 | - `question-simple.component.ts` 343 | - `question-simple.html` 344 | - `question-simple.css` (optional) 345 | - `index.ts` 346 | 347 | ```ts 348 | // question-simple.component.ts 349 | 350 | import { Component } from '@angular/core'; 351 | import { GenericQuestion, QuestionType } from 'app/components'; 352 | 353 | import template from './question-simple.html'; 354 | import mainStyle from './question-simple.css'; 355 | import commonStyle from '../common.css'; 356 | 357 | @Component({ 358 | selector: 'question-simple', 359 | template: template, 360 | styles: [ 361 | commonStyle, 362 | mainStyle 363 | ] 364 | }) 365 | export class QuestionSimpleComponent extends GenericQuestion { 366 | 367 | public static type = QuestionType.Simple; 368 | 369 | public init(): void { 370 | this.setTitle('Do you like this quiz?'); 371 | this.setCorrectAnswer('yes'); 372 | } 373 | 374 | } 375 | ``` 376 | 377 | ```html 378 | 379 | 380 |
381 |
382 |
383 |
384 | sentiment_satisfied 385 |
386 |
387 | {{ question.title }} 388 |
389 |
390 |
391 |
392 | 393 | 394 | 395 | 396 | 399 | 400 | sentiment_satisfied 401 | 402 | 403 | ``` 404 | 405 | ```ts 406 | // index.ts 407 | export * from './question-simple.component'; 408 | ``` 409 | 410 | Finally add an export to `src/js/components/quiz/questions/questions.ts`: 411 | 412 | ```ts 413 | export * from './simple/index'; 414 | ``` 415 | 416 | ### This is it 417 | 418 | The application will automatically consider the added question and will use it randomly. 419 | 420 | > Try it! You can copy-paste the code above. 421 | -------------------------------------------------------------------------------- /base.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var assign = require('lodash.assign'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | BUILD CONFIGURATION 6 | |-------------------------------------------------------------------------- 7 | | 8 | | All configurations concerning the build go here. 9 | | 10 | | Note that you can always use an array for src, to be more 11 | | specific or to exclude files. 12 | | 13 | | TypeScript Lint Configuration: 14 | | - tslint.json 15 | | 16 | | TypeScript Configutation: 17 | | - tsconfig.json 18 | | 19 | | Reserved: 20 | | - config.env 21 | | - config.mode 22 | | - config.build 23 | | 24 | */ 25 | var config = config || {}; 26 | 27 | // Use the build timestamp to prevent browser caching of new versions 28 | config.buildTimestamp = new Date().valueOf(); 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | App Base Href 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Set the href attribute and the base for 36 | | the application itself (e.g. routing) 37 | | 38 | */ 39 | config.htmlBaseHref = '/'; 40 | config.appBaseHref = '/'; 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Source and Distribution Location 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Define the location of the source and distribution folder. 48 | | 49 | */ 50 | config.src = './src'; 51 | config.dist = './dist'; 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Watch Files 56 | |-------------------------------------------------------------------------- 57 | | 58 | | Define files, which "gulp watch" should keep an eye on. 59 | | 60 | */ 61 | config.watch = ['src/**/*', 'index.html']; 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | SystemJS Builder 66 | |-------------------------------------------------------------------------- 67 | | 68 | | You can pass SystemJS builder/JSPM options, and overwrite those, set in 69 | | gulpfile.js 70 | | 71 | */ 72 | config.jspm = { 73 | bundles: [ 74 | { 75 | options: [ 76 | 'build', 77 | 'reflect-metadata + zone.js + app - app/mock/**/*', 78 | config.dist + '/js/app.js', 79 | '--minify', 80 | '--skip-source-maps' 81 | ], 82 | devOptions: [ 83 | 'build', 84 | 'reflect-metadata + zone.js + app/main.dev.ts', 85 | config.dist + '/js/app.js', 86 | '--no-mangle' 87 | ] 88 | } 89 | ] 90 | }; 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | TypeScript Linter 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Define files to check for errors. 98 | | All options are set in tslint.json. 99 | | 100 | */ 101 | config.tslint = { 102 | src: config.src + '/js/**/*.ts' 103 | }; 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | LESS Configuration 108 | |-------------------------------------------------------------------------- 109 | | 110 | | Define the source and destination path, as well as the 111 | | concatinated file name. You may also use less for styleUrls 112 | | within angular. 113 | | 114 | */ 115 | config.less = { 116 | src: config.src + '/css/app.less', 117 | dest: config.dist + '/css', 118 | name: 'app.css' 119 | }; 120 | 121 | /* |-------------------------------------------------------------------------- 122 | | Index File Configuration (Main Entry Point) 123 | |-------------------------------------------------------------------------- 124 | | 125 | | Define the source and destination path, as well as the 126 | | file name. 127 | | */ 128 | config.index = { 129 | src: './index.html', 130 | dest: config.dist, 131 | name: 'index.html' 132 | }; 133 | 134 | /* 135 | |-------------------------------------------------------------------------- 136 | | Assets Configuration 137 | |-------------------------------------------------------------------------- 138 | | 139 | | Define the source and destination path, as well as the 140 | | concatinated file name. Files will be copied as-is. 141 | | 142 | */ 143 | config.assets = { 144 | src: config.src + '/assets/**/*', 145 | dest: config.dist + '/assets' 146 | }; 147 | 148 | /* 149 | |-------------------------------------------------------------------------- 150 | | Copy Configuration 151 | |-------------------------------------------------------------------------- 152 | | 153 | | Define additional files that should be copied. 154 | | 155 | */ 156 | config.copy = [ 157 | { 158 | base: './node_modules/materialize-css/fonts/roboto', 159 | src: ['/*'], 160 | dest: config.dist + '/assets/fonts/roboto' 161 | }, 162 | { 163 | base: './node_modules/flat-color-icons/svg', 164 | src: ['/*.svg'], 165 | dest: config.dist + '/assets/icons' 166 | }, 167 | { 168 | base: './node_modules/material-design-icons/iconfont', 169 | src: ['/*.eot', '/*.woff2', '/*.woff', '/*.ttf'], 170 | dest: config.dist + '/assets/fonts/iconfont' 171 | } 172 | ]; 173 | 174 | module.exports = config; 175 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redirecting to fabiandev.io/angular-quiz-app 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Dependencies 4 | |-------------------------------------------------------------------------- 5 | | 6 | | Below you can add all files and plugins you need within 7 | | your tasks. 8 | | 9 | */ 10 | var gulp = require('gulp'); 11 | var gulpif = require('gulp-if'); 12 | var tslint = require('gulp-tslint'); 13 | var less = require('gulp-less'); 14 | var cleanCSS = require('gulp-clean-css'); 15 | var concat = require('gulp-concat'); 16 | var filter = require('gulp-filter'); 17 | var sourcemaps = require('gulp-sourcemaps'); 18 | var preprocess = require('gulp-preprocess'); 19 | var rename = require('gulp-rename'); 20 | var pp = require('preprocess'); 21 | var argv = require('yargs').argv; 22 | var del = require('del'); 23 | var exec = require('node-exec-promise').exec; 24 | var log = require('fancy-log'); 25 | var colors = require('ansi-colors'); 26 | var notifier = require('node-notifier'); 27 | var path = require('path'); 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Global Definitions 32 | |-------------------------------------------------------------------------- 33 | | 34 | | All configurations can be defined in config.js. The environment 35 | | will be set automatically and can be changed by using either 36 | | "gulp build" or "gulp dev-build". 37 | | 38 | | Also all other global definitions go here, like defining the 39 | | typescript project. 40 | | 41 | */ 42 | var config = require('./config'); 43 | config.env = process.env.NODE_ENV; 44 | config.mode = config.env !== 'production' ? 'bundle' : 'build'; 45 | config.build = true; 46 | 47 | // Determine environment before it is set for initialization 48 | process.env.NODE_ENV = config.env = 49 | argv._[0] === 'build' ? 'production' : 'development'; 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Internal Tasks 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Tasks are defined below, that are used internally: 57 | | 58 | | - clean:* 59 | | Deletes the specific files, based on the clean task. 60 | | 61 | | - builder/jspm 62 | | Builds and bundles the application via SystemJS. 63 | | 64 | | - less 65 | | Compiles less files and saves it into the distribution 66 | | folder. 67 | | 68 | | - copy:* 69 | | Copies files according to the task name. 70 | | 71 | | - bundle:* 72 | | Bundles files according to the task name. 73 | | 74 | | - lint:* 75 | | Lints specific types of files. 76 | | 77 | */ 78 | 79 | gulp.task('clean:all', function() { 80 | return del([path.normalize(path.join(config.dist, '**/*'))]); 81 | }); 82 | 83 | gulp.task('clean:scripts', function() { 84 | return del([ 85 | path.normalize(path.join(config.dist, 'js/**/*.js')), 86 | path.normalize(path.join(config.dist, 'js/**/*.map')) 87 | ]); 88 | }); 89 | 90 | gulp.task('clean:vendor', function() { 91 | return del([path.normalize(path.join(config.vendor.dest, '**/*'))]); 92 | }); 93 | 94 | gulp.task('clean:styles', function() { 95 | return del([ 96 | path.normalize(path.join(config.dist, 'css/**/*.css')), 97 | path.normalize(path.join(config.dist, 'css/**/*.map')) 98 | ]); 99 | }); 100 | 101 | gulp.task('clean:index', function() { 102 | return del([path.normalize(path.join(config.dist, 'index.html'))]); 103 | }); 104 | 105 | gulp.task('clean:html', function() { 106 | return del([path.normalize(path.join(config.dist, '**/*.html'))]); 107 | }); 108 | 109 | gulp.task('clean:assets', function() { 110 | return del([path.normalize(path.join(config.dist, 'assets/**/*'))]); 111 | }); 112 | 113 | gulp.task('jspm', function(done) { 114 | var bundles = []; 115 | 116 | config.jspm.bundles.forEach(function(bundle) { 117 | var command = 118 | config.env !== 'production' ? bundle.devOptions : bundle.options; 119 | command = command.slice(0); 120 | command.unshift('node_modules/.bin/jspm'); 121 | 122 | bundles.push(exec(command.join(' '))); 123 | }); 124 | 125 | Promise.all(bundles).then( 126 | function(values) { 127 | var file = path.join(config.dist, 'js/app.js'); 128 | var context = { config: config }; 129 | var options = { type: 'js' }; 130 | 131 | pp.preprocessFile(file, file, context, done, options); 132 | }, 133 | function(error) { 134 | onError('Error bundling with jspm.', error); 135 | } 136 | ); 137 | }); 138 | 139 | gulp.task('less', function() { 140 | return gulp 141 | .src(config.less.src) 142 | .pipe( 143 | gulpif( 144 | config.env !== 'production', 145 | sourcemaps.init().on('error', onError) 146 | ) 147 | ) 148 | .pipe(less().on('error', onError)) 149 | .pipe(cleanCSS().on('error', onError)) 150 | .pipe(rename(config.less.name).on('error', onError)) 151 | .pipe( 152 | gulpif( 153 | config.env !== 'production', 154 | sourcemaps.write('./').on('error', onError) 155 | ) 156 | ) 157 | .pipe(gulp.dest(config.less.dest)) 158 | .on('error', onError); 159 | }); 160 | 161 | gulp.task('copy:index', function() { 162 | return gulp 163 | .src(config.index.src) 164 | .pipe( 165 | preprocess({ 166 | context: { 167 | config: config 168 | } 169 | }).on('error', onError) 170 | ) 171 | .pipe(rename(config.index.name).on('error', onError)) 172 | .pipe(gulp.dest(config.index.dest)) 173 | .on('error', onError); 174 | }); 175 | 176 | gulp.task('copy:assets', function() { 177 | return gulp 178 | .src(config.assets.src) 179 | .pipe(gulp.dest(config.assets.dest)) 180 | .on('error', onError); 181 | }); 182 | 183 | var copyTasks = []; 184 | 185 | function createCopyTask(element, index) { 186 | var name = 'copy:originals:' + index; 187 | var sources = []; 188 | 189 | element.src.forEach(function(p) { 190 | sources.push(path.join(element.base, p)); 191 | }); 192 | 193 | gulp.task(name, function() { 194 | return gulp 195 | .src(sources) 196 | .pipe(gulp.dest(element.dest), { base: element.base }) 197 | .on('error', onError); 198 | }); 199 | 200 | copyTasks.push(name); 201 | } 202 | 203 | clone(config.copy).forEach(function(element, index) { 204 | createCopyTask(element, index); 205 | }); 206 | 207 | gulp.task('copy:originals', gulp.series(...clone(copyTasks))); 208 | 209 | gulp.task('lint:ts', function() { 210 | return gulp 211 | .src([config.tslint.src]) 212 | .pipe( 213 | tslint({ 214 | formatter: 'stylish' 215 | }) 216 | ) 217 | .pipe(tslint.report()) 218 | .on('error', notifyError); 219 | }); 220 | 221 | /* 222 | |-------------------------------------------------------------------------- 223 | | Helper Tasks 224 | |-------------------------------------------------------------------------- 225 | | 226 | | The following tasks are used as helpers. 227 | | 228 | */ 229 | gulp.task('set-dev', function() { 230 | return Promise.resolve(process.env.NODE_ENV = config.env = 'development'); 231 | }); 232 | gulp.task('set-prod', function(done) { 233 | return Promise.resolve(process.env.NODE_ENV = config.env = 'production'); 234 | }); 235 | 236 | gulp.task('start', function(done) { 237 | log(colors.green('Starting ' + config.env + ' build...')); 238 | 239 | return done(); 240 | }); 241 | 242 | gulp.task('finish', function(done) { 243 | log(colors.green('Build has finished.')); 244 | 245 | notifier.notify({ 246 | title: 'Build Successful', 247 | message: 'All build tasks have finished and your app is ready.' 248 | }); 249 | 250 | return done(); 251 | }); 252 | 253 | /* 254 | |-------------------------------------------------------------------------- 255 | | Helper Functions 256 | |-------------------------------------------------------------------------- 257 | | 258 | | Simple functions for different purposes. 259 | | 260 | */ 261 | 262 | function onError(error, details) { 263 | log(colors.red('Error: ' + error)); 264 | if (details) { 265 | log(details); 266 | } 267 | notifyError(error); 268 | } 269 | 270 | function notifyError(error) { 271 | notifier.notify({ 272 | title: 'Error', 273 | message: 'There was an error building your app.' 274 | }); 275 | } 276 | 277 | function clone(obj) { 278 | return JSON.parse(JSON.stringify(obj)); 279 | } 280 | 281 | /* 282 | |-------------------------------------------------------------------------- 283 | | Task Collections 284 | |-------------------------------------------------------------------------- 285 | | 286 | | The tasks below bundle common sequences: 287 | | 288 | | - tasks 289 | | The main task sequence. 290 | | 291 | | - copy 292 | | Should bundle all tasks prefixed with "copy:". 293 | | 294 | | - clean:default 295 | | Only clear scripts (excluding vendor), styles and the index. 296 | | 297 | | - bundle 298 | | Execute all available bundle tasks. 299 | | 300 | | - lint 301 | | Run all available lint tasks. 302 | | 303 | | - typescript 304 | | Runs all required typescript tasks. 305 | | 306 | */ 307 | 308 | gulp.task('copy', gulp.series('copy:index', 'copy:assets', 'copy:originals')); 309 | 310 | gulp.task('tasks', gulp.series('copy', 'less')); 311 | 312 | gulp.task('clean:default', gulp.series('clean:scripts', 'clean:styles', 'clean:index')); 313 | 314 | gulp.task('lint', gulp.series('lint:ts')); 315 | 316 | gulp.task('master', gulp.series('lint', 'clean:all', gulp.parallel('tasks', 'jspm'))); 317 | 318 | /* 319 | |-------------------------------------------------------------------------- 320 | | Main Tasks 321 | |-------------------------------------------------------------------------- 322 | | 323 | | These tasks are intended to be called via the console: 324 | | 325 | | - build 326 | | Performs a production build. 327 | | 328 | | - dev-build 329 | | Performs a development build. 330 | | A dev build performs the same tasks as 331 | | a prod build, but with the env set to 332 | | dev, which may result in different 333 | | behaviors in tasks. 334 | | 335 | | - watch-build 336 | | Tasks that should run if a file changes. 337 | | 338 | | - watch 339 | | Performs a development build everytime 340 | | something has changes. 341 | | 342 | | - serve 343 | | Brings up a server, to test the app 344 | | locally. 345 | | 346 | */ 347 | 348 | gulp.task('build', gulp.series('set-prod', 'start', 'master', 'finish')); 349 | 350 | gulp.task('dev-build', gulp.series('set-dev', 'start', 'master', 'finish')); 351 | 352 | gulp.task('watch-build', gulp.series('set-dev', 'start', 'lint', 'clean:default', gulp.parallel('tasks'), 'finish')); 353 | 354 | gulp.task('watch', function() { 355 | gulpSequence('watch-build')(function() { 356 | gulp.watch(config.watch, ['watch-build']); 357 | }); 358 | }); 359 | 360 | gulp.task('serve', function(done) { 361 | log(colors.blue('Use "npm start [dev]" instead.')); 362 | done(); 363 | }); 364 | 365 | gulp.task('default', gulp.series('build')); 366 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spotify Music Quiz with Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 38 | 39 | 40 | 41 | 42 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-quiz-app", 3 | "version": "1.10.1", 4 | "description": "A music quiz in Angular 2+ using the Spotify API.", 5 | "main": "server/index.js", 6 | "scripts": { 7 | "postinstall": "node_modules/.bin/jspm install && npm run update-paths && npm run build", 8 | "heroku-postbuild": "npm run prune", 9 | "update-paths": "node_modules/.bin/jspm-tsc-update", 10 | "prune": "node_modules/.bin/jspm clean && npm prune --production", 11 | "update": "yarn upgrade && jspm update && npm run update-paths", 12 | "build": "node_modules/.bin/gulp build", 13 | "start": "node server" 14 | }, 15 | "engines": { 16 | "node": ">=6" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/fabiandev/angular-quiz-app.git" 21 | }, 22 | "author": "Fabian Pirklbauer ", 23 | "contributors": [ 24 | "Sascha Zarhuber", 25 | "Lorenz Krautgartner" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/fabiandev/angular-quiz-app/issues" 30 | }, 31 | "homepage": "https://github.com/fabiandev/angular-quiz-app#readme", 32 | "dependencies": { 33 | "animate.css": "^3.5.2", 34 | "body-parser": "^1.15.0", 35 | "compression": "^1.6.1", 36 | "errorhandler": "^1.4.3", 37 | "express": "^4.13.4", 38 | "method-override": "^3.0.0", 39 | "newrelic": "^5.9.1", 40 | "node-env-file": "^0.1.8", 41 | "node-notifier": "^5.1.2", 42 | "spotify-web-api-node": "^4.0.0", 43 | "yargs": "^13.2.4" 44 | }, 45 | "devDependencies": { 46 | "@types/core-js": "^2.5.1", 47 | "@types/hammerjs": "^2.0.36", 48 | "@types/iscroll": "^5.2.2", 49 | "@types/materialize-css": "^0.100.0", 50 | "ansi-colors": "^3.2.4", 51 | "del": "^4.1.1", 52 | "fancy-log": "^1.3.2", 53 | "flat-color-icons": "^1.1.0", 54 | "gulp": "^4.0.2", 55 | "gulp-clean-css": "^4.2.0", 56 | "gulp-concat": "^2.6.0", 57 | "gulp-filter": "^6.0.0", 58 | "gulp-if": "^2.0.0", 59 | "gulp-less": "^4.0.1", 60 | "gulp-preprocess": "^3.0.2", 61 | "gulp-rename": "^1.2.2", 62 | "gulp-sourcemaps": "^2.6.0", 63 | "gulp-tslint": "^8.1.1", 64 | "jspm": "^0.17.0-beta.49", 65 | "jspm-tsc-update": "^0.2.0-beta.2", 66 | "less": "^3.9.0", 67 | "lodash.assign": "^4.0.6", 68 | "material-design-icons": "^3.0.1", 69 | "materialize-css": "^0.98.2", 70 | "node-exec-promise": "^1.0.2", 71 | "normalize.css": "^8.0.1", 72 | "tslint": "^5.4.3", 73 | "typescript": "^3.5.1" 74 | }, 75 | "jspm": { 76 | "configFiles": { 77 | "jspm": "system.config.js" 78 | }, 79 | "configFile": "system.config.js", 80 | "dependencies": { 81 | "@angular/forms": "npm:@angular/forms@^8.0.0", 82 | "@angular/http": "npm:@angular/http@^7.2.15", 83 | "@angular/platform-browser-dynamic": "npm:@angular/platform-browser-dynamic@^8.0.0", 84 | "@angular/router": "npm:@angular/router@^8.0.0", 85 | "angular2-materialize": "npm:angular2-materialize@^15.0.1", 86 | "css-animator": "npm:css-animator@^2.3.1", 87 | "hammerjs": "npm:hammerjs@^2.0.8", 88 | "iscroll": "npm:iscroll@^5.2.0", 89 | "jquery": "npm:jquery@^2.2.4", 90 | "reflect-metadata": "npm:reflect-metadata@^0.1.13" 91 | }, 92 | "devDependencies": { 93 | "core-js": "npm:core-js@^3.1.3", 94 | "source-map-support": "npm:source-map-support@^0.5.12", 95 | "ts-runtime": "npm:babel-runtime@^6.26.0", 96 | "text": "github:systemjs/plugin-text@^0.0.11", 97 | "ts": "github:frankwallis/plugin-typescript@^9.0.0", 98 | "plugin-babel": "npm:systemjs-plugin-babel@^0.0.25", 99 | "typescript": "npm:typescript@^3.0.3" 100 | }, 101 | "peerDependencies": { 102 | "@angular/common": "npm:@angular/common@^8.0.0", 103 | "@angular/compiler": "npm:@angular/compiler@^8.0.0", 104 | "@angular/core": "npm:@angular/core@8.0.0", 105 | "@angular/platform-browser": "npm:@angular/platform-browser@^8.0.0", 106 | "assert": "npm:jspm-nodelibs-assert@^0.2.0", 107 | "buffer": "npm:jspm-nodelibs-buffer@^0.2.0", 108 | "child_process": "npm:jspm-nodelibs-child_process@^0.2.0", 109 | "constants": "npm:jspm-nodelibs-constants@^0.2.0", 110 | "crypto": "npm:jspm-nodelibs-crypto@^0.2.0", 111 | "events": "npm:jspm-nodelibs-events@^0.2.0", 112 | "fs": "npm:jspm-nodelibs-fs@^0.2.0", 113 | "http": "npm:jspm-nodelibs-http@^0.2.0", 114 | "materialize-css": "npm:materialize-css@^0.100.1", 115 | "module": "npm:jspm-nodelibs-module@^0.2.0", 116 | "net": "npm:jspm-nodelibs-net@^0.2.0", 117 | "os": "npm:jspm-nodelibs-os@^0.2.0", 118 | "path": "npm:jspm-nodelibs-path@^0.2.0", 119 | "process": "npm:jspm-nodelibs-process@^0.2.0", 120 | "readline": "npm:jspm-nodelibs-readline@^0.2.0", 121 | "rxjs": "npm:rxjs@^6.4.0", 122 | "stream": "npm:jspm-nodelibs-stream@^0.2.0", 123 | "string_decoder": "npm:jspm-nodelibs-string_decoder@^0.2.0", 124 | "timers": "npm:jspm-nodelibs-timers@^0.2.0", 125 | "tty": "npm:jspm-nodelibs-tty@^0.2.0", 126 | "url": "npm:jspm-nodelibs-url@^0.2.0", 127 | "util": "npm:jspm-nodelibs-util@^0.2.0", 128 | "vm": "npm:jspm-nodelibs-vm@^0.2.0", 129 | "zlib": "npm:jspm-nodelibs-zlib@^0.2.0", 130 | "zone.js": "npm:zone.js@~0.9.1" 131 | }, 132 | "overrides": { 133 | "npm:browserify-zlib@0.1.4": { 134 | "dependencies": { 135 | "readable-stream": "^2.0.2", 136 | "pako": "~0.2.0" 137 | }, 138 | "map": { 139 | "_stream_transform": "readable-stream/transform" 140 | } 141 | }, 142 | "npm:inherits@2.0.3": { 143 | "ignore": [ 144 | "test.js" 145 | ] 146 | }, 147 | "npm:jquery@2.2.4": { 148 | "format": "amd" 149 | }, 150 | "npm:pbkdf2@3.0.17": { 151 | "main": "browser.js" 152 | }, 153 | "npm:reflect-metadata@0.1.13": { 154 | "jspmNodeConversion": true, 155 | "map": {} 156 | }, 157 | "npm:safe-buffer@5.1.2": { 158 | "browser": "index.js" 159 | }, 160 | "npm:tslib@1.10.0": { 161 | "format": "cjs", 162 | "meta": { 163 | "tslib.js": { 164 | "esModule": true 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /production.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | # NEVER INCLUDE THE REAL ENVIRONMENT VARIABLES FILE 2 | # IN SOURCE CONTROL AS IT MAY CONTAIN SECRET KEYS!! 3 | 4 | API_BASE_PATH=/ 5 | 6 | NODE_ENV=production 7 | NPM_CONFIG_PRODUCTION=false 8 | NEW_RELIC_LOG=stdout 9 | 10 | NEW_RELIC_LICENSE_KEY= 11 | SPOTIFY_CLIENT_ID= 12 | SPOTIFY_CLIENT_SECRET= 13 | -------------------------------------------------------------------------------- /server/bootstrap.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log('Starting server...'); 4 | 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const env = require('node-env-file'); 8 | const config = require('./config'); 9 | 10 | const envFile = path.join(__dirname, '/.env'); 11 | 12 | let envFileExists = fs.existsSync(envFile); 13 | 14 | if (!config.production && !envFileExists) { 15 | fs.writeFileSync(envFile, fs.readFileSync(`${envFile}.example`)); 16 | } 17 | 18 | if (envFileExists) { 19 | env(`${__dirname}/.env`); 20 | console.log(`Loaded env vars from file.`); 21 | } 22 | -------------------------------------------------------------------------------- /server/config/base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicDir: '../', 3 | env: process.env.NODE_ENV, 4 | production: process.env.NODE_ENV === 'production' 5 | }; 6 | -------------------------------------------------------------------------------- /server/config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | httpsRedirect: false 3 | }; 4 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | var base = require('./base'); 2 | 3 | if (process.argv.indexOf('dev') !== -1) { 4 | module.exports = Object.assign({}, base, require('./dev')); 5 | } else { 6 | module.exports = Object.assign({}, base, require('./prod')); 7 | } 8 | -------------------------------------------------------------------------------- /server/config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicDir: '../dist', 3 | httpsRedirect: true 4 | }; 5 | -------------------------------------------------------------------------------- /server/controller/spotify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const https = require('https'); 4 | const SpotifyWebApi = require('spotify-web-api-node'); 5 | 6 | class Spotify { 7 | constructor() { 8 | this.clientId = process.env.SPOTIFY_CLIENT_ID; 9 | this.clientSecret = process.env.SPOTIFY_CLIENT_SECRET; 10 | this.redirectUri = process.env.SPOTIFY_REDIRECT_URI; 11 | this.accessToken = null; 12 | 13 | this.api = new SpotifyWebApi({ 14 | clientId: this.clientId, 15 | clientSecret: this.clientSecret, 16 | redirectUri: this.redirectUri 17 | }); 18 | } 19 | 20 | getRandomEntry(arr) { 21 | if (!Array.isArray(arr)) { 22 | return false; 23 | } 24 | let len = arr.length; 25 | let pos = Math.floor(Math.random() * len); 26 | return arr[pos]; 27 | } 28 | 29 | createQueryString(obj) { 30 | if (typeof obj === 'object' && !Array.isArray(obj) && obj !== null) { 31 | let qs = []; 32 | for (let key in obj) { 33 | qs.push(key + '=' + obj[key]); 34 | } 35 | return qs.join('&'); 36 | } 37 | return false; 38 | } 39 | 40 | isTokenValid() { 41 | let tokenTo = this.tokenTimeout || Date.now() - 3600; 42 | let d = new Date(); 43 | let to = new Date(tokenTo); 44 | return to > d; 45 | } 46 | 47 | fetchToken() { 48 | return new Promise((resolve, reject) => { 49 | if (this.accessToken === null || !this.isTokenValid()) { 50 | this.api.clientCredentialsGrant() 51 | .then(d => { 52 | this.accessToken = d.body['access_token']; 53 | this.tokenTimeout = Date.now() + 3500; // Initially keep 100ms buffer before timeout 54 | this.api.setAccessToken(d.body['access_token']); 55 | resolve(d.body['access_token']); 56 | }) 57 | .catch(e => reject('Error while fetching access token: ' + e)); 58 | } else { 59 | resolve(this.accessToken); 60 | } 61 | }); 62 | } 63 | 64 | fetchPlaylistData(user, id) { 65 | return new Promise((resolve, reject) => { 66 | if (!user || !id) { 67 | reject('Necessary data missing, User: ' + user + ', Playlist: ' + id); 68 | } 69 | this.fetchToken() 70 | .then(d => this.api.getPlaylist(user, id)) 71 | .then(d => resolve(d.body)) 72 | .catch(e => reject('Error while fetching playlist data: ' + e)); 73 | }); 74 | } 75 | 76 | fetchFeaturedPlaylists(obj) { 77 | obj = obj || {}; 78 | obj.country = !obj.country ? 'US' : obj.country.toUpperCase(); 79 | obj.locale = obj.locale || 'en_US'; 80 | obj.limit = obj.limit || 20; 81 | obj.offset = obj.offset || 0; 82 | obj.timestamp = obj.timestamp || new Date(); 83 | obj.timestamp = new Date(obj.timestamp).toISOString(); 84 | 85 | return new Promise((resolve, reject) => { 86 | this.fetchToken() 87 | .then(d => this.api.getFeaturedPlaylists({ 88 | country: obj.country, 89 | locale: obj.locale, 90 | limit: obj.limit, 91 | offset: obj.offset, 92 | timestamp: obj.timestamp 93 | }) 94 | ) 95 | .then(d => resolve(d.body)) 96 | .catch(e => reject('Error while fetching playlist: ' + e)); 97 | }); 98 | } 99 | 100 | fetchSearchedPlaylists(obj) { 101 | return new Promise((resolve, reject) => { 102 | if (!obj || !obj.query ) { 103 | reject('No search query given...'); 104 | } else { 105 | obj.q = encodeURIComponent(obj.query); 106 | obj.limit = obj.limit || 20; 107 | obj.offset = obj.offset || 0; 108 | obj.type = 'playlist'; 109 | obj.market = !obj.country ? 'US' : obj.country.toUpperCase(); 110 | 111 | delete(obj.query); 112 | delete(obj.country); 113 | 114 | let qs = this.createQueryString(obj); 115 | if (!qs) { 116 | reject('No valid query given...'); 117 | } 118 | 119 | this.fetchToken() 120 | .then(d => { 121 | let opts = { 122 | protocol: 'https:', 123 | hostname: 'api.spotify.com', 124 | path: '/v1/search?' + qs, 125 | headers: { 126 | 'Authorization' : 'Bearer ' + this.accessToken 127 | } 128 | } 129 | let xhr = https.get(opts, res => { 130 | let data = ''; 131 | res.on('data', d => data += d); 132 | res.on('end', () => resolve(JSON.parse(data))); 133 | }); 134 | xhr.on('error', e => reject('Error while retrieving search data: ' + e)); 135 | }) 136 | .catch(e => reject(e)); 137 | } 138 | }); 139 | } 140 | 141 | 142 | } 143 | 144 | module.exports = new Spotify(); 145 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./bootstrap'); 4 | 5 | if (process.env.NEW_RELIC_LICENSE_KEY) { 6 | console.log('Enabling New Relic...'); 7 | require('newrelic'); 8 | } else { 9 | console.log('New Relic not enabled.'); 10 | } 11 | 12 | const express = require('express'); 13 | const path = require('path'); 14 | const bodyParser = require('body-parser'); 15 | const errorHandler = require('errorhandler'); 16 | const methodOverride = require('method-override'); 17 | const compress = require('compression'); 18 | const config = require('./config'); 19 | const app = express(); 20 | 21 | // Define font mime types 22 | express.static.mime.define({ 23 | 'application/x-font-woff': ['woff'], 24 | 'application/x-font-woff2': ['woff2'], 25 | 'application/x-font-ttf': ['ttf'] 26 | }); 27 | 28 | console.log(''); 29 | console.log(`Server environment is ${process.env.NODE_ENV}`); 30 | console.log(`Server public dir set to ${config.publicDir}`); 31 | 32 | var basePath = process.env.API_BASE_PATH ? process.env.API_BASE_PATH : '/'; 33 | 34 | // Compress responses 35 | app.use(compress()); 36 | 37 | // Request middleware 38 | 39 | if (config.httpsRedirect) { 40 | app.use(require('./middleware/httpsRedirect')); 41 | } 42 | 43 | app.use(require('./middleware/isFile')); 44 | app.use(express.static(path.join(__dirname, config.publicDir))); 45 | 46 | // Routes 47 | app.use(require('./routes/routes')); 48 | app.use(path.join(basePath, 'api'), require('./routes/api')); 49 | 50 | // Serve index if file does not exist 51 | app.get('*', function(req, res, next) { 52 | if (!req.isFile) { 53 | return res.sendFile('index.html', { 54 | root: path.join(__dirname, config.publicDir) 55 | }); 56 | } 57 | 58 | return next(); 59 | }); 60 | 61 | // Response middleware 62 | app.use(methodOverride()); 63 | app.use(bodyParser.json()); 64 | app.use( 65 | bodyParser.urlencoded({ 66 | extended: true 67 | }) 68 | ); 69 | app.use( 70 | errorHandler({ 71 | dumpExceptions: !config.production, 72 | showStack: !config.production 73 | }) 74 | ); 75 | 76 | // Use port 5000 in development 77 | var port = config.production ? process.env.PORT : 5000; 78 | port = !port ? 5000 : port; 79 | 80 | // Start the server 81 | app.listen(port); 82 | 83 | console.log(`Server listening on port ${port}`); 84 | console.log(''); 85 | -------------------------------------------------------------------------------- /server/middleware/httpsRedirect.js: -------------------------------------------------------------------------------- 1 | module.exports = function redirectToHttps(req, res, next) { 2 | if (!/https/.test(req.protocol)) { 3 | return res.redirect("https://" + req.headers.host + req.url); 4 | } 5 | 6 | return next(); 7 | } 8 | -------------------------------------------------------------------------------- /server/middleware/isFile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var config = require('../config/base'); 3 | 4 | module.exports = function isFile(req, res, next) { 5 | fs.stat(config.publicDir + req.url, function(err, stats) { 6 | req.isFile = stats && stats.isFile(); 7 | next(); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /server/newrelic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.config = { 4 | app_name: ['hueapp'], 5 | license_key: process.env.NEW_RELIC_LICENSE_KEY, 6 | logging: { 7 | level: 'info', 8 | filepath: process.env.NEW_RELIC_LOG 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const router = require('express').Router(); 4 | const spotify = require('../controller/spotify'); 5 | 6 | function isObject(item) { 7 | return (typeof item === "object" && !Array.isArray(item) && item !== null); 8 | } 9 | 10 | function createErrRes(err) { 11 | let error = isObject(err) ? err : { message: err }; 12 | let statusCode = err && err.statusCode ? err.statusCode : 500; 13 | 14 | error.name = error.name || 'ApiError'; 15 | error.message = error.message || 'Whoops, something went wrong. That\'s all we know :('; 16 | error.statusCode = error.statusCode || statusCode; 17 | 18 | return error; 19 | } 20 | 21 | router.get('/playlist/random', function(req, res) { 22 | /* 23 | * GET parameters allowed: 24 | * ---------------------- 25 | * country, locale, limit, offset, timestamp 26 | */ 27 | spotify.fetchFeaturedPlaylists(req.query) 28 | .then(d => spotify.getRandomEntry(d.playlists.items)) 29 | .then(d => spotify.fetchPlaylistData(d.owner.id, d.id)) 30 | .then(d => res.json(d)) 31 | .catch(err => { 32 | let error = createErrRes(err); 33 | return res.status(error.statusCode).json({ 34 | error: error 35 | }); 36 | }); 37 | }); 38 | 39 | router.get('/playlist/search', function(req, res) { 40 | /* 41 | * GET parameters allowed: 42 | * ---------------------- 43 | * query, country, limit, offset 44 | */ 45 | spotify.fetchSearchedPlaylists(req.query) 46 | .then(d => spotify.getRandomEntry(d.playlists.items)) 47 | .then(d => spotify.fetchPlaylistData(d.owner.id, d.id)) 48 | .then(d => res.json(d)) 49 | .catch(err => { 50 | let error = createErrRes(err); 51 | return res.status(error.statusCode).json({ 52 | error: error 53 | }); 54 | }); 55 | }); 56 | 57 | router.get('/playlist/data', function(req, res) { 58 | if (!req.query.userId || !req.query.playlistId) { 59 | let error = createErrRes({ 60 | statusCode: 400, 61 | name: "ClientError", 62 | message: "userId and/or playlistId not provided." 63 | }); 64 | return res.status(error.statusCode).json(error); 65 | } 66 | spotify.fetchPlaylistData(req.query.userId, req.query.playlistId) 67 | .then(d => res.json(d)) 68 | .catch(err => { 69 | let error = createErrRes(err); 70 | return res.status(error.statusCode).json({ 71 | error: error 72 | }); 73 | }); 74 | }); 75 | 76 | module.exports = router; 77 | -------------------------------------------------------------------------------- /server/routes/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var router = require('express').Router(); 4 | 5 | // define middleware and routes here 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/mock/questions.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "type": 1, 3 | "title": "What is the name of this song?", 4 | "description": "You can play 30 seconds of the song.", 5 | "answers": [{ 6 | "title": "Nothing Is Promised", 7 | "correct": false 8 | }, { 9 | "title": "Sing Me to Sleep", 10 | "correct": true 11 | }, { 12 | "title": "Millionaire (feat. Nelly)", 13 | "correct": false 14 | }, { 15 | "title": "Let It Burn", 16 | "correct": false 17 | }], 18 | "answered": false, 19 | "status": { 20 | "answered": false 21 | }, 22 | "track": { 23 | "album": { 24 | "album_type": "single", 25 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 26 | "external_urls": { 27 | "spotify": "https://open.spotify.com/album/6v6EzwVvKfFZfThip9675y" 28 | }, 29 | "href": "https://api.spotify.com/v1/albums/6v6EzwVvKfFZfThip9675y", 30 | "id": "6v6EzwVvKfFZfThip9675y", 31 | "images": [{ 32 | "height": 640, 33 | "url": "https://i.scdn.co/image/0af2ace9f8bf254a1b79b7c331dd3e8cfdf61101", 34 | "width": 640 35 | }, { 36 | "height": 300, 37 | "url": "https://i.scdn.co/image/2704a3bcd9f7ffaee7af10e4e93e67820366d88a", 38 | "width": 300 39 | }, { 40 | "height": 64, 41 | "url": "https://i.scdn.co/image/8e317e8271de4642f0b69257af36db2c5b6f01e4", 42 | "width": 64 43 | }], 44 | "name": "Sing Me to Sleep", 45 | "type": "album", 46 | "uri": "spotify:album:6v6EzwVvKfFZfThip9675y" 47 | }, 48 | "artists": [{ 49 | "external_urls": { 50 | "spotify": "https://open.spotify.com/artist/7vk5e3vY1uw9plTHJAMwjN" 51 | }, 52 | "href": "https://api.spotify.com/v1/artists/7vk5e3vY1uw9plTHJAMwjN", 53 | "id": "7vk5e3vY1uw9plTHJAMwjN", 54 | "name": "Alan Walker", 55 | "type": "artist", 56 | "uri": "spotify:artist:7vk5e3vY1uw9plTHJAMwjN" 57 | }], 58 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 59 | "disc_number": 1, 60 | "duration_ms": 189333, 61 | "explicit": false, 62 | "external_ids": { 63 | "isrc": "NOG841611010" 64 | }, 65 | "external_urls": { 66 | "spotify": "https://open.spotify.com/track/6UcTHiP25meOD9ox8Rspgx" 67 | }, 68 | "href": "https://api.spotify.com/v1/tracks/6UcTHiP25meOD9ox8Rspgx", 69 | "id": "6UcTHiP25meOD9ox8Rspgx", 70 | "name": "Sing Me to Sleep", 71 | "popularity": 0, 72 | "preview_url": "https://p.scdn.co/mp3-preview/80a367d5be11f63bfa48550bdd33c6d327c4daf4", 73 | "track_number": 1, 74 | "type": "track", 75 | "uri": "spotify:track:6UcTHiP25meOD9ox8Rspgx" 76 | }, 77 | "id": 1 78 | }, { 79 | "type": 0, 80 | "title": "What is the name of this album?", 81 | "answers": [{ 82 | "title": "Give It Up", 83 | "correct": false 84 | }, { 85 | "title": "Seal The Deal & Let's Boogie", 86 | "correct": true 87 | }, { 88 | "title": "Volition", 89 | "correct": false 90 | }, { 91 | "title": "Stadium Pow Wow", 92 | "correct": false 93 | }], 94 | "answered": false, 95 | "status": { 96 | "answered": false 97 | }, 98 | "track": { 99 | "album": { 100 | "album_type": "album", 101 | "available_markets": ["CA", "US", "MX"], 102 | "external_urls": { 103 | "spotify": "https://open.spotify.com/album/43UPS43heoPmecwkeQhAnI" 104 | }, 105 | "href": "https://api.spotify.com/v1/albums/43UPS43heoPmecwkeQhAnI", 106 | "id": "43UPS43heoPmecwkeQhAnI", 107 | "images": [{ 108 | "height": 640, 109 | "url": "https://i.scdn.co/image/5840d6b635ea1659370d88c146bc175fef84f8ad", 110 | "width": 640 111 | }, { 112 | "height": 300, 113 | "url": "https://i.scdn.co/image/f80f3bf89f63d5f89fdff34b02abe03ecf05a9bb", 114 | "width": 300 115 | }, { 116 | "height": 64, 117 | "url": "https://i.scdn.co/image/16bf23e64927debdc4b9e314e4c53a46b9f8524c", 118 | "width": 64 119 | }], 120 | "name": "Seal The Deal & Let's Boogie", 121 | "type": "album", 122 | "uri": "spotify:album:43UPS43heoPmecwkeQhAnI" 123 | }, 124 | "artists": [{ 125 | "external_urls": { 126 | "spotify": "https://open.spotify.com/artist/0L5fC7Ogm2YwgqVCRcF1bT" 127 | }, 128 | "href": "https://api.spotify.com/v1/artists/0L5fC7Ogm2YwgqVCRcF1bT", 129 | "id": "0L5fC7Ogm2YwgqVCRcF1bT", 130 | "name": "Volbeat", 131 | "type": "artist", 132 | "uri": "spotify:artist:0L5fC7Ogm2YwgqVCRcF1bT" 133 | }], 134 | "available_markets": ["CA", "US", "MX"], 135 | "disc_number": 1, 136 | "duration_ms": 219426, 137 | "explicit": false, 138 | "external_ids": { 139 | "isrc": "DKNE11600105" 140 | }, 141 | "external_urls": { 142 | "spotify": "https://open.spotify.com/track/7GPWhYyfUvbpB11NXeAhhc" 143 | }, 144 | "href": "https://api.spotify.com/v1/tracks/7GPWhYyfUvbpB11NXeAhhc", 145 | "id": "7GPWhYyfUvbpB11NXeAhhc", 146 | "name": "Let It Burn", 147 | "popularity": 0, 148 | "preview_url": "https://p.scdn.co/mp3-preview/266951717de86c46aa0f5ad7e638d60814b3d739", 149 | "track_number": 5, 150 | "type": "track", 151 | "uri": "spotify:track:7GPWhYyfUvbpB11NXeAhhc" 152 | }, 153 | "id": 2 154 | }, { 155 | "type": 0, 156 | "title": "What is the name of this album?", 157 | "answers": [{ 158 | "title": "Take Me Dancing", 159 | "correct": false 160 | }, { 161 | "title": "Waking - EP", 162 | "correct": false 163 | }, { 164 | "title": "Bacon", 165 | "correct": false 166 | }, { 167 | "title": "Back Where I Belong (feat. Avicii)", 168 | "correct": true 169 | }], 170 | "answered": false, 171 | "status": { 172 | "answered": false 173 | }, 174 | "track": { 175 | "album": { 176 | "album_type": "single", 177 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 178 | "external_urls": { 179 | "spotify": "https://open.spotify.com/album/6c4fND91gsKojyp3Mox97y" 180 | }, 181 | "href": "https://api.spotify.com/v1/albums/6c4fND91gsKojyp3Mox97y", 182 | "id": "6c4fND91gsKojyp3Mox97y", 183 | "images": [{ 184 | "height": 640, 185 | "url": "https://i.scdn.co/image/22c87706c2811fd6ba57fff3e10090accf8b0fde", 186 | "width": 640 187 | }, { 188 | "height": 300, 189 | "url": "https://i.scdn.co/image/720ea7c896ecccc8e22b6092981a89465794833c", 190 | "width": 300 191 | }, { 192 | "height": 64, 193 | "url": "https://i.scdn.co/image/0751c95e4d5bde4513092e1eda5fc915eadaa503", 194 | "width": 64 195 | }], 196 | "name": "Back Where I Belong (feat. Avicii)", 197 | "type": "album", 198 | "uri": "spotify:album:6c4fND91gsKojyp3Mox97y" 199 | }, 200 | "artists": [{ 201 | "external_urls": { 202 | "spotify": "https://open.spotify.com/artist/5fahUm8t5c0GIdeTq0ZaG8" 203 | }, 204 | "href": "https://api.spotify.com/v1/artists/5fahUm8t5c0GIdeTq0ZaG8", 205 | "id": "5fahUm8t5c0GIdeTq0ZaG8", 206 | "name": "Otto Knows", 207 | "type": "artist", 208 | "uri": "spotify:artist:5fahUm8t5c0GIdeTq0ZaG8" 209 | }, { 210 | "external_urls": { 211 | "spotify": "https://open.spotify.com/artist/1vCWHaC5f2uS3yhpwWbIA6" 212 | }, 213 | "href": "https://api.spotify.com/v1/artists/1vCWHaC5f2uS3yhpwWbIA6", 214 | "id": "1vCWHaC5f2uS3yhpwWbIA6", 215 | "name": "Avicii", 216 | "type": "artist", 217 | "uri": "spotify:artist:1vCWHaC5f2uS3yhpwWbIA6" 218 | }], 219 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 220 | "disc_number": 1, 221 | "duration_ms": 174202, 222 | "explicit": false, 223 | "external_ids": { 224 | "isrc": "SE4OX1600112" 225 | }, 226 | "external_urls": { 227 | "spotify": "https://open.spotify.com/track/78W8wiUIlQ2SnWY9TVowKZ" 228 | }, 229 | "href": "https://api.spotify.com/v1/tracks/78W8wiUIlQ2SnWY9TVowKZ", 230 | "id": "78W8wiUIlQ2SnWY9TVowKZ", 231 | "name": "Back Where I Belong (feat. Avicii)", 232 | "popularity": 0, 233 | "preview_url": "https://p.scdn.co/mp3-preview/e7195b01a7ab0bb0af748bb46d00ef2ad4dd9b7f", 234 | "track_number": 1, 235 | "type": "track", 236 | "uri": "spotify:track:78W8wiUIlQ2SnWY9TVowKZ" 237 | }, 238 | "id": 3 239 | }, { 240 | "type": 2, 241 | "title": "What is the name of this album?", 242 | "answers": [{ 243 | "title": "Stadium Pow Wow", 244 | "correct": false 245 | }, { 246 | "title": "Spotify Sessions", 247 | "correct": true 248 | }, { 249 | "title": "Youth (feat. Satica)", 250 | "correct": false 251 | }, { 252 | "title": "Beautiful Life", 253 | "correct": false 254 | }], 255 | "answered": false, 256 | "status": { 257 | "answered": false 258 | }, 259 | "track": { 260 | "album": { 261 | "album_type": "album", 262 | "available_markets": ["AD", "AU", "BE", "BG", "CY", "CZ", "EE", "ES", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MY", "NL", "NZ", "PH", "PL", "PT", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 263 | "external_urls": { 264 | "spotify": "https://open.spotify.com/album/7gsnn7SqpfGZ1x7FVDElQP" 265 | }, 266 | "href": "https://api.spotify.com/v1/albums/7gsnn7SqpfGZ1x7FVDElQP", 267 | "id": "7gsnn7SqpfGZ1x7FVDElQP", 268 | "images": [{ 269 | "height": 640, 270 | "url": "https://i.scdn.co/image/423165d4d6d4c2c8a36714070eff6010c64e0006", 271 | "width": 640 272 | }, { 273 | "height": 300, 274 | "url": "https://i.scdn.co/image/59727cc24161984d6f1d6840644890bef486be16", 275 | "width": 300 276 | }, { 277 | "height": 64, 278 | "url": "https://i.scdn.co/image/c67cbd3a2d8b4cac232591d463cba455a63b70bc", 279 | "width": 64 280 | }], 281 | "name": "Spotify Sessions", 282 | "type": "album", 283 | "uri": "spotify:album:7gsnn7SqpfGZ1x7FVDElQP" 284 | }, 285 | "artists": [{ 286 | "external_urls": { 287 | "spotify": "https://open.spotify.com/artist/25u4wHJWxCA9vO0CzxAbK7" 288 | }, 289 | "href": "https://api.spotify.com/v1/artists/25u4wHJWxCA9vO0CzxAbK7", 290 | "id": "25u4wHJWxCA9vO0CzxAbK7", 291 | "name": "Lukas Graham", 292 | "type": "artist", 293 | "uri": "spotify:artist:25u4wHJWxCA9vO0CzxAbK7" 294 | }], 295 | "available_markets": ["AD", "AU", "BE", "BG", "CY", "CZ", "EE", "ES", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MY", "NL", "NZ", "PH", "PL", "PT", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 296 | "disc_number": 1, 297 | "duration_ms": 253813, 298 | "explicit": false, 299 | "external_ids": { 300 | "isrc": "USWB11600823" 301 | }, 302 | "external_urls": { 303 | "spotify": "https://open.spotify.com/track/794tmuXAOSHhC4tDrkc2IX" 304 | }, 305 | "href": "https://api.spotify.com/v1/tracks/794tmuXAOSHhC4tDrkc2IX", 306 | "id": "794tmuXAOSHhC4tDrkc2IX", 307 | "name": "Mama Said - Spotify Sessions", 308 | "popularity": 0, 309 | "preview_url": "https://p.scdn.co/mp3-preview/c80324440d4cca7de8b36200eb23c8b2875974e3", 310 | "track_number": 7, 311 | "type": "track", 312 | "uri": "spotify:track:794tmuXAOSHhC4tDrkc2IX" 313 | }, 314 | "id": 4 315 | }, { 316 | "type": 0, 317 | "title": "What is the name of this album?", 318 | "answers": [{ 319 | "title": "Palms", 320 | "correct": false 321 | }, { 322 | "title": "Take Me Dancing", 323 | "correct": true 324 | }, { 325 | "title": "Nothing Is Promised", 326 | "correct": false 327 | }, { 328 | "title": "Back Where I Belong (feat. Avicii)", 329 | "correct": false 330 | }], 331 | "answered": false, 332 | "status": { 333 | "answered": false 334 | }, 335 | "track": { 336 | "album": { 337 | "album_type": "single", 338 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 339 | "external_urls": { 340 | "spotify": "https://open.spotify.com/album/2RgB03BigvGTy53HG9sRWk" 341 | }, 342 | "href": "https://api.spotify.com/v1/albums/2RgB03BigvGTy53HG9sRWk", 343 | "id": "2RgB03BigvGTy53HG9sRWk", 344 | "images": [{ 345 | "height": 640, 346 | "url": "https://i.scdn.co/image/f2d4e1d253b56f0ab3431e3b4f93444d54c47097", 347 | "width": 640 348 | }, { 349 | "height": 300, 350 | "url": "https://i.scdn.co/image/1cce721c2afe7145d47bd1891448c5c603312517", 351 | "width": 300 352 | }, { 353 | "height": 64, 354 | "url": "https://i.scdn.co/image/51e1f596d2d51080c20c319bc70171ac9054b00a", 355 | "width": 64 356 | }], 357 | "name": "Take Me Dancing", 358 | "type": "album", 359 | "uri": "spotify:album:2RgB03BigvGTy53HG9sRWk" 360 | }, 361 | "artists": [{ 362 | "external_urls": { 363 | "spotify": "https://open.spotify.com/artist/3YO2a6i2cfdFbgxk2HDfPe" 364 | }, 365 | "href": "https://api.spotify.com/v1/artists/3YO2a6i2cfdFbgxk2HDfPe", 366 | "id": "3YO2a6i2cfdFbgxk2HDfPe", 367 | "name": "Will Joseph Cook", 368 | "type": "artist", 369 | "uri": "spotify:artist:3YO2a6i2cfdFbgxk2HDfPe" 370 | }], 371 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 372 | "disc_number": 1, 373 | "duration_ms": 189565, 374 | "explicit": false, 375 | "external_ids": { 376 | "isrc": "GBAHS1600227" 377 | }, 378 | "external_urls": { 379 | "spotify": "https://open.spotify.com/track/2eUAW2QOrLzxQvURcP5WcX" 380 | }, 381 | "href": "https://api.spotify.com/v1/tracks/2eUAW2QOrLzxQvURcP5WcX", 382 | "id": "2eUAW2QOrLzxQvURcP5WcX", 383 | "name": "Take Me Dancing", 384 | "popularity": 0, 385 | "preview_url": "https://p.scdn.co/mp3-preview/92d44411cda0349d68212cef507fb68e00181a71", 386 | "track_number": 1, 387 | "type": "track", 388 | "uri": "spotify:track:2eUAW2QOrLzxQvURcP5WcX" 389 | }, 390 | "id": 5 391 | }, { 392 | "type": 3, 393 | "title": "Who is the artist of this album?", 394 | "answers": [{ 395 | "title": "Will Joseph Cook", 396 | "correct": false 397 | }, { 398 | "title": "Weathers", 399 | "correct": false 400 | }, { 401 | "title": "Maren Morris", 402 | "correct": false 403 | }, { 404 | "title": "A Tribe Called Red", 405 | "correct": true 406 | }], 407 | "answered": false, 408 | "status": { 409 | "answered": false 410 | }, 411 | "track": { 412 | "album": { 413 | "album_type": "single", 414 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 415 | "external_urls": { 416 | "spotify": "https://open.spotify.com/album/3cwbXVO5bTH5KuYibc4oSr" 417 | }, 418 | "href": "https://api.spotify.com/v1/albums/3cwbXVO5bTH5KuYibc4oSr", 419 | "id": "3cwbXVO5bTH5KuYibc4oSr", 420 | "images": [{ 421 | "height": 640, 422 | "url": "https://i.scdn.co/image/1a2625803f2dc21f26c3216dcad9bc931591a952", 423 | "width": 640 424 | }, { 425 | "height": 300, 426 | "url": "https://i.scdn.co/image/5b7e71888c11b22db3605161d6e44cadab8eff41", 427 | "width": 300 428 | }, { 429 | "height": 64, 430 | "url": "https://i.scdn.co/image/a657ca2687739d4ca2e3c3612bd9c3249f3b9093", 431 | "width": 64 432 | }], 433 | "name": "Stadium Pow Wow", 434 | "type": "album", 435 | "uri": "spotify:album:3cwbXVO5bTH5KuYibc4oSr" 436 | }, 437 | "artists": [{ 438 | "external_urls": { 439 | "spotify": "https://open.spotify.com/artist/2jlWF9ltd8UtoaqW0PxY4z" 440 | }, 441 | "href": "https://api.spotify.com/v1/artists/2jlWF9ltd8UtoaqW0PxY4z", 442 | "id": "2jlWF9ltd8UtoaqW0PxY4z", 443 | "name": "A Tribe Called Red", 444 | "type": "artist", 445 | "uri": "spotify:artist:2jlWF9ltd8UtoaqW0PxY4z" 446 | }, { 447 | "external_urls": { 448 | "spotify": "https://open.spotify.com/artist/0yA6zmKmNXjUPSZyfDZji7" 449 | }, 450 | "href": "https://api.spotify.com/v1/artists/0yA6zmKmNXjUPSZyfDZji7", 451 | "id": "0yA6zmKmNXjUPSZyfDZji7", 452 | "name": "Black Bear", 453 | "type": "artist", 454 | "uri": "spotify:artist:0yA6zmKmNXjUPSZyfDZji7" 455 | }], 456 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE", "CR", "GT", "HN", "NI", "SV"], 457 | "disc_number": 1, 458 | "duration_ms": 207562, 459 | "explicit": false, 460 | "external_ids": { 461 | "isrc": "CAVP11500011" 462 | }, 463 | "external_urls": { 464 | "spotify": "https://open.spotify.com/track/0C3nr1sZn8n4UtUaMMRyoZ" 465 | }, 466 | "href": "https://api.spotify.com/v1/tracks/0C3nr1sZn8n4UtUaMMRyoZ", 467 | "id": "0C3nr1sZn8n4UtUaMMRyoZ", 468 | "name": "Stadium Pow Wow", 469 | "popularity": 0, 470 | "preview_url": "https://p.scdn.co/mp3-preview/a0a12336d613e99156195549776760ba45a717eb", 471 | "track_number": 1, 472 | "type": "track", 473 | "uri": "spotify:track:0C3nr1sZn8n4UtUaMMRyoZ" 474 | }, 475 | "id": 6 476 | }, { 477 | "type": 0, 478 | "title": "What is the name of this album?", 479 | "answers": [{ 480 | "title": "Youth (feat. Satica)", 481 | "correct": false 482 | }, { 483 | "title": "Volition", 484 | "correct": false 485 | }, { 486 | "title": "Maybe IDK", 487 | "correct": false 488 | }, { 489 | "title": "Frankie Sinatra (Extended Mix)", 490 | "correct": true 491 | }], 492 | "answered": false, 493 | "status": { 494 | "answered": false 495 | }, 496 | "track": { 497 | "album": { 498 | "album_type": "single", 499 | "available_markets": ["CA", "MX", "US"], 500 | "external_urls": { 501 | "spotify": "https://open.spotify.com/album/7bvvsC0PTzI9ws7vLBVLqi" 502 | }, 503 | "href": "https://api.spotify.com/v1/albums/7bvvsC0PTzI9ws7vLBVLqi", 504 | "id": "7bvvsC0PTzI9ws7vLBVLqi", 505 | "images": [{ 506 | "height": 640, 507 | "url": "https://i.scdn.co/image/93bf9cc615921611f379d3f4ee2f9e98efe4ce1a", 508 | "width": 640 509 | }, { 510 | "height": 300, 511 | "url": "https://i.scdn.co/image/fea6814c4fb5dbd086da455bf8bf06bdc7c0d416", 512 | "width": 300 513 | }, { 514 | "height": 64, 515 | "url": "https://i.scdn.co/image/41734e6b5142d0eee78bb7709dd0bf8cb541f0d2", 516 | "width": 64 517 | }], 518 | "name": "Frankie Sinatra (Extended Mix)", 519 | "type": "album", 520 | "uri": "spotify:album:7bvvsC0PTzI9ws7vLBVLqi" 521 | }, 522 | "artists": [{ 523 | "external_urls": { 524 | "spotify": "https://open.spotify.com/artist/3C8RpaI3Go0yFF9whvKoED" 525 | }, 526 | "href": "https://api.spotify.com/v1/artists/3C8RpaI3Go0yFF9whvKoED", 527 | "id": "3C8RpaI3Go0yFF9whvKoED", 528 | "name": "The Avalanches", 529 | "type": "artist", 530 | "uri": "spotify:artist:3C8RpaI3Go0yFF9whvKoED" 531 | }], 532 | "available_markets": ["CA", "MX", "US"], 533 | "disc_number": 1, 534 | "duration_ms": 268080, 535 | "explicit": true, 536 | "external_ids": { 537 | "isrc": "AUUM71600247" 538 | }, 539 | "external_urls": { 540 | "spotify": "https://open.spotify.com/track/1ar8ZvWo0gkk93TExlKY7X" 541 | }, 542 | "href": "https://api.spotify.com/v1/tracks/1ar8ZvWo0gkk93TExlKY7X", 543 | "id": "1ar8ZvWo0gkk93TExlKY7X", 544 | "name": "Frankie Sinatra - Extended Mix", 545 | "popularity": 0, 546 | "preview_url": "https://p.scdn.co/mp3-preview/63e01d171ebf2dacd306b55ed99abb03d8a523e9", 547 | "track_number": 1, 548 | "type": "track", 549 | "uri": "spotify:track:1ar8ZvWo0gkk93TExlKY7X" 550 | }, 551 | "id": 7 552 | }, { 553 | "type": 3, 554 | "title": "Who is the artist of this album?", 555 | "answers": [{ 556 | "title": "Weathers", 557 | "correct": false 558 | }, { 559 | "title": "Lukas Graham", 560 | "correct": false 561 | }, { 562 | "title": "Cash Cash", 563 | "correct": false 564 | }, { 565 | "title": "Maxwell", 566 | "correct": true 567 | }], 568 | "answered": false, 569 | "status": { 570 | "answered": false 571 | }, 572 | "track": { 573 | "album": { 574 | "album_type": "single", 575 | "available_markets": ["US"], 576 | "external_urls": { 577 | "spotify": "https://open.spotify.com/album/5R8cx9bzPNg5DJyw5VEdj0" 578 | }, 579 | "href": "https://api.spotify.com/v1/albums/5R8cx9bzPNg5DJyw5VEdj0", 580 | "id": "5R8cx9bzPNg5DJyw5VEdj0", 581 | "images": [{ 582 | "height": 640, 583 | "url": "https://i.scdn.co/image/a9cf1d7f079b7cb82f11efb99a935340784f8e37", 584 | "width": 640 585 | }, { 586 | "height": 300, 587 | "url": "https://i.scdn.co/image/2093fdecb99678268a5ea9680301bab3e7cec6e0", 588 | "width": 300 589 | }, { 590 | "height": 64, 591 | "url": "https://i.scdn.co/image/8e667271b4779049c444caf7259144a1cd285791", 592 | "width": 64 593 | }], 594 | "name": "1990x", 595 | "type": "album", 596 | "uri": "spotify:album:5R8cx9bzPNg5DJyw5VEdj0" 597 | }, 598 | "artists": [{ 599 | "external_urls": { 600 | "spotify": "https://open.spotify.com/artist/2AOt5htsbtyaHd5Eq3kl3j" 601 | }, 602 | "href": "https://api.spotify.com/v1/artists/2AOt5htsbtyaHd5Eq3kl3j", 603 | "id": "2AOt5htsbtyaHd5Eq3kl3j", 604 | "name": "Maxwell", 605 | "type": "artist", 606 | "uri": "spotify:artist:2AOt5htsbtyaHd5Eq3kl3j" 607 | }], 608 | "available_markets": ["US"], 609 | "disc_number": 1, 610 | "duration_ms": 284693, 611 | "explicit": false, 612 | "external_ids": { 613 | "isrc": "USSM11602521" 614 | }, 615 | "external_urls": { 616 | "spotify": "https://open.spotify.com/track/12KJ1qRNaxqCaQ2ju7wU0q" 617 | }, 618 | "href": "https://api.spotify.com/v1/tracks/12KJ1qRNaxqCaQ2ju7wU0q", 619 | "id": "12KJ1qRNaxqCaQ2ju7wU0q", 620 | "name": "1990x", 621 | "popularity": 0, 622 | "preview_url": "https://p.scdn.co/mp3-preview/29cf0e38a70de8e8a733faafb95cbc15b01d1700", 623 | "track_number": 1, 624 | "type": "track", 625 | "uri": "spotify:track:12KJ1qRNaxqCaQ2ju7wU0q" 626 | }, 627 | "id": 8 628 | }, { 629 | "type": 2, 630 | "title": "What is the name of this album?", 631 | "answers": [{ 632 | "title": "My Environment", 633 | "correct": false 634 | }, { 635 | "title": "Good Girls (from the \"Ghostbusters\" Original Motion Picture Soundtrack)", 636 | "correct": false 637 | }, { 638 | "title": "Wild Things", 639 | "correct": false 640 | }, { 641 | "title": "Future Present Past", 642 | "correct": true 643 | }], 644 | "answered": false, 645 | "status": { 646 | "answered": false 647 | }, 648 | "track": { 649 | "album": { 650 | "album_type": "single", 651 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE"], 652 | "external_urls": { 653 | "spotify": "https://open.spotify.com/album/2OkdkfVKqje8mj7g1iLroV" 654 | }, 655 | "href": "https://api.spotify.com/v1/albums/2OkdkfVKqje8mj7g1iLroV", 656 | "id": "2OkdkfVKqje8mj7g1iLroV", 657 | "images": [{ 658 | "height": 640, 659 | "url": "https://i.scdn.co/image/3a43fea00877375ff97c270a44058b393a7c3b8e", 660 | "width": 640 661 | }, { 662 | "height": 300, 663 | "url": "https://i.scdn.co/image/2b8a79bd6db3b8e4344f86f0d058b06e0db4d814", 664 | "width": 300 665 | }, { 666 | "height": 64, 667 | "url": "https://i.scdn.co/image/2021474f9c8a143988b7d4252d1f6bd0f5356388", 668 | "width": 64 669 | }], 670 | "name": "Future Present Past", 671 | "type": "album", 672 | "uri": "spotify:album:2OkdkfVKqje8mj7g1iLroV" 673 | }, 674 | "artists": [{ 675 | "external_urls": { 676 | "spotify": "https://open.spotify.com/artist/0epOFNiUfyON9EYx7Tpr6V" 677 | }, 678 | "href": "https://api.spotify.com/v1/artists/0epOFNiUfyON9EYx7Tpr6V", 679 | "id": "0epOFNiUfyON9EYx7Tpr6V", 680 | "name": "The Strokes", 681 | "type": "artist", 682 | "uri": "spotify:artist:0epOFNiUfyON9EYx7Tpr6V" 683 | }], 684 | "available_markets": ["AD", "AT", "AU", "BE", "BG", "CH", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HK", "HU", "ID", "IE", "IS", "IT", "LI", "LT", "LU", "LV", "MC", "MT", "MY", "NL", "NO", "NZ", "PH", "PL", "PT", "SE", "SG", "SK", "TR", "TW", "AR", "BR", "UY", "BO", "CA", "CL", "DO", "PY", "US", "CO", "EC", "MX", "PA", "PE"], 685 | "disc_number": 1, 686 | "duration_ms": 299157, 687 | "explicit": false, 688 | "external_ids": { 689 | "isrc": "QMKBG1600002" 690 | }, 691 | "external_urls": { 692 | "spotify": "https://open.spotify.com/track/2tjqgiScVM3VoZBYGPC2Dm" 693 | }, 694 | "href": "https://api.spotify.com/v1/tracks/2tjqgiScVM3VoZBYGPC2Dm", 695 | "id": "2tjqgiScVM3VoZBYGPC2Dm", 696 | "name": "OBLIVIUS", 697 | "popularity": 61, 698 | "preview_url": "https://p.scdn.co/mp3-preview/1a82cc951f006983abd81a0df76b4b4cf17c7aa8", 699 | "track_number": 2, 700 | "type": "track", 701 | "uri": "spotify:track:2tjqgiScVM3VoZBYGPC2Dm" 702 | }, 703 | "id": 9 704 | }, { 705 | "type": 1, 706 | "title": "What is the name of this song?", 707 | "description": "You can play 30 seconds of the song.", 708 | "answers": [{ 709 | "title": "Blood. Sweat. Tears. - John Lock Remix", 710 | "correct": true 711 | }, { 712 | "title": "GA$", 713 | "correct": false 714 | }, { 715 | "title": "Safe Inside (feat. Passion Pit)", 716 | "correct": false 717 | }, { 718 | "title": "Take Me Dancing", 719 | "correct": false 720 | }], 721 | "answered": false, 722 | "status": { 723 | "answered": false 724 | }, 725 | "track": { 726 | "album": { 727 | "album_type": "single", 728 | "available_markets": ["AR", "AU", "AT", "BE", "BO", "BR", "BG", "CA", "CL", "CO", "CR", "CY", "CZ", "DK", "DO", "DE", "EC", "EE", "SV", "FI", "FR", "GR", "GT", "HN", "HK", "HU", "IS", "IE", "IT", "LV", "LT", "LU", "MY", "MT", "MX", "NL", "NZ", "NI", "NO", "PA", "PY", "PE", "PH", "PL", "PT", "SG", "SK", "ES", "SE", "CH", "TW", "TR", "UY", "US", "GB", "AD", "LI", "MC", "ID"], 729 | "external_urls": { 730 | "spotify": "https://open.spotify.com/album/4El05DUyoHcQX9bijTHMMb" 731 | }, 732 | "href": "https://api.spotify.com/v1/albums/4El05DUyoHcQX9bijTHMMb", 733 | "id": "4El05DUyoHcQX9bijTHMMb", 734 | "images": [{ 735 | "height": 640, 736 | "url": "https://i.scdn.co/image/f77d578caedfbbad5d252bf9f244e50fda0b7eaf", 737 | "width": 640 738 | }, { 739 | "height": 300, 740 | "url": "https://i.scdn.co/image/239881491b7e71493815901b43f35ac968bcdce2", 741 | "width": 300 742 | }, { 743 | "height": 64, 744 | "url": "https://i.scdn.co/image/5141ddddd5f1d5ec53ce988fc17dc4a6b4c1a28b", 745 | "width": 64 746 | }], 747 | "name": "Blood. Sweat. Tears. (John Lock Remix) - Single", 748 | "type": "album", 749 | "uri": "spotify:album:4El05DUyoHcQX9bijTHMMb" 750 | }, 751 | "artists": [{ 752 | "external_urls": { 753 | "spotify": "https://open.spotify.com/artist/0mEUqATSCsYZUTyL09kB8g" 754 | }, 755 | "href": "https://api.spotify.com/v1/artists/0mEUqATSCsYZUTyL09kB8g", 756 | "id": "0mEUqATSCsYZUTyL09kB8g", 757 | "name": "Molly Moore", 758 | "type": "artist", 759 | "uri": "spotify:artist:0mEUqATSCsYZUTyL09kB8g" 760 | }], 761 | "available_markets": ["AR", "AU", "AT", "BE", "BO", "BR", "BG", "CA", "CL", "CO", "CR", "CY", "CZ", "DK", "DO", "DE", "EC", "EE", "SV", "FI", "FR", "GR", "GT", "HN", "HK", "HU", "IS", "IE", "IT", "LV", "LT", "LU", "MY", "MT", "MX", "NL", "NZ", "NI", "NO", "PA", "PY", "PE", "PH", "PL", "PT", "SG", "SK", "ES", "SE", "CH", "TW", "TR", "UY", "US", "GB", "AD", "LI", "MC", "ID"], 762 | "disc_number": 1, 763 | "duration_ms": 249230, 764 | "explicit": false, 765 | "external_ids": { 766 | "isrc": "USUYG1103293" 767 | }, 768 | "external_urls": { 769 | "spotify": "https://open.spotify.com/track/0mIdQnrTO7JLL3G7OR30yY" 770 | }, 771 | "href": "https://api.spotify.com/v1/tracks/0mIdQnrTO7JLL3G7OR30yY", 772 | "id": "0mIdQnrTO7JLL3G7OR30yY", 773 | "name": "Blood. Sweat. Tears. - John Lock Remix", 774 | "popularity": 4, 775 | "preview_url": "https://p.scdn.co/mp3-preview/392eb51b21a5db502e4f673ae43110d97fe7595d", 776 | "track_number": 1, 777 | "type": "track", 778 | "uri": "spotify:track:0mIdQnrTO7JLL3G7OR30yY" 779 | }, 780 | "id": 10 781 | }] 782 | -------------------------------------------------------------------------------- /src/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/css/.gitkeep -------------------------------------------------------------------------------- /src/css/app.less: -------------------------------------------------------------------------------- 1 | @import "variables/index"; 2 | @import "vendor/index"; 3 | @import "styles/index"; 4 | -------------------------------------------------------------------------------- /src/css/styles/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Roboto-Fix"; 3 | src: local(Roboto Thin), url("../assets/fonts/roboto/Roboto-Thin.eot"); 4 | src: url("../assets/fonts/roboto/Roboto-Thin.eot?#iefix") format("embedded-opentype"), url("../assets/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("../assets/fonts/roboto/Roboto-Thin.woff") format("woff"), url("../assets/fonts/roboto/Roboto-Thin.ttf") format("truetype"); 5 | font-weight: 200; 6 | } 7 | 8 | @font-face { 9 | font-family: "Roboto-Fix"; 10 | src: local(Roboto Light), url("../assets/fonts/roboto/Roboto-Light.eot"); 11 | src: url("../assets/fonts/roboto/Roboto-Light.eot?#iefix") format("embedded-opentype"), url("../assets/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("../assets/fonts/roboto/Roboto-Light.woff") format("woff"), url("../assets/fonts/roboto/Roboto-Light.ttf") format("truetype"); 12 | font-weight: 300; 13 | } 14 | 15 | @font-face { 16 | font-family: "Roboto-Fix"; 17 | src: local(Roboto Regular), url("../assets/fonts/roboto/Roboto-Regular.eot"); 18 | src: url("../assets/fonts/roboto/Roboto-Regular.eot?#iefix") format("embedded-opentype"), url("../assets/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("../assets/fonts/roboto/Roboto-Regular.woff") format("woff"), url("../assets/fonts/roboto/Roboto-Regular.ttf") format("truetype"); 19 | font-weight: 400; 20 | } 21 | 22 | @font-face { 23 | font-family: "Roboto-Fix"; 24 | src: url("../assets/fonts/roboto/Roboto-Medium.eot"); 25 | src: url("../assets/fonts/roboto/Roboto-Medium.eot?#iefix") format("embedded-opentype"), url("../assets/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("../assets/fonts/roboto/Roboto-Medium.woff") format("woff"), url("../assets/fonts/roboto/Roboto-Medium.ttf") format("truetype"); 26 | font-weight: 500; 27 | } 28 | 29 | @font-face { 30 | font-family: "Roboto-Fix"; 31 | src: url("../assets/fonts/roboto/Roboto-Bold.eot"); 32 | src: url("../assets/fonts/roboto/Roboto-Bold.eot?#iefix") format("embedded-opentype"), url("../assets/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("../assets/fonts/roboto/Roboto-Bold.woff") format("woff"), url("../assets/fonts/roboto/Roboto-Bold.ttf") format("truetype"); 33 | font-weight: 700; 34 | } 35 | -------------------------------------------------------------------------------- /src/css/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "./fonts"; 2 | @import "./material-icons"; 3 | @import "./materialize-normalize"; 4 | @import "./main"; 5 | @import "./loading-animation"; 6 | -------------------------------------------------------------------------------- /src/css/styles/loading-animation.less: -------------------------------------------------------------------------------- 1 | @keyframes ldboxrotate { 2 | to { 3 | transform: rotate(1turn); 4 | } 5 | } 6 | 7 | #app-loading { 8 | font-family: Helvetica, Arial, sans-serif; 9 | z-index: 10001; 10 | background-color: #00bcd4; 11 | position: fixed; 12 | top: 0; 13 | right: 0; 14 | bottom: 0; 15 | left: 0; 16 | // display: flex; 17 | // flex-direction: column; 18 | // justify-content: center; 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | .loader { 24 | left: 50%; 25 | top: 50%; 26 | margin-top: -100px; // put further to top than center 27 | margin-left: -30px; 28 | position: absolute; 29 | border: 3px solid transparent; 30 | border-top: 3px solid #fff; 31 | border-bottom: 3px solid #fff; 32 | border-left: 3px solid #fff; 33 | border-right: 3px solid #fff; 34 | border-radius: 60px; 35 | animation-fill-mode: both; 36 | animation-name: ldboxrotate; 37 | animation-timing-function: linear; 38 | animation-iteration-count: 1; 39 | animation-play-state: paused; 40 | animation-duration: 0.8s; 41 | height: 60px; 42 | width: 60px; 43 | } 44 | 45 | .loader.running { 46 | animation-iteration-count: infinite; 47 | animation-play-state: running; 48 | } 49 | 50 | .loader:after { 51 | content: ''; 52 | position: absolute; 53 | width: 30px; 54 | height: 33px; 55 | top: 30px; 56 | left: 15px; 57 | background-color: #00bcd4; 58 | } 59 | -------------------------------------------------------------------------------- /src/css/styles/main.less: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: "Roboto-Fix", sans-serif !important; 3 | } 4 | 5 | ul.dropdown-content li>a, ul.dropdown-content li>span { 6 | color: #00bcd4; 7 | } 8 | 9 | ul.dropdown-content li.selected, ul.dropdown-content li:hover { 10 | background-color: rgba(236, 239, 241, 0.6); 11 | } 12 | -------------------------------------------------------------------------------- /src/css/styles/material-icons.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url("../assets/fonts/iconfont/MaterialIcons-Regular.eot"); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url("../assets/fonts/iconfont/MaterialIcons-Regular.woff2") format('woff2'), 9 | url("../assets/fonts/iconfont/MaterialIcons-Regular.woff") format('woff'), 10 | url("../assets/fonts/iconfont/MaterialIcons-Regular.ttf") format('truetype'); 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | // font-size: 24px; /* Preferred icon size */ 18 | display: inline-block; 19 | line-height: 1; 20 | text-transform: none; 21 | letter-spacing: normal; 22 | word-wrap: normal; 23 | white-space: nowrap; 24 | direction: ltr; 25 | 26 | /* Support for all WebKit browsers. */ 27 | -webkit-font-smoothing: antialiased; 28 | /* Support for Safari and Chrome. */ 29 | text-rendering: optimizeLegibility; 30 | 31 | /* Support for Firefox. */ 32 | -moz-osx-font-smoothing: grayscale; 33 | 34 | /* Support for IE. */ 35 | font-feature-settings: 'liga'; 36 | } 37 | -------------------------------------------------------------------------------- /src/css/styles/materialize-normalize.less: -------------------------------------------------------------------------------- 1 | html { 2 | line-height: 1.5; 3 | font-size: 15px; 4 | box-sizing: border-box; 5 | } 6 | 7 | *, *:before, *:after { 8 | box-sizing: inherit; 9 | } 10 | -------------------------------------------------------------------------------- /src/css/variables/index.less: -------------------------------------------------------------------------------- 1 | @node_modules-path: "../../../node_modules"; 2 | -------------------------------------------------------------------------------- /src/css/vendor/index.less: -------------------------------------------------------------------------------- 1 | @import (inline) "@{node_modules-path}/normalize.css/normalize.css"; 2 | @import (inline) "@{node_modules-path}/animate.css/animate.min.css"; 3 | @import (inline) "@{node_modules-path}/materialize-css/dist/css/materialize.css"; 4 | -------------------------------------------------------------------------------- /src/js/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import * as Hammer from 'hammerjs'; 2 | (window as any).Hammer = Hammer; 3 | 4 | import { AnimationBuilder } from 'css-animator/builder'; 5 | import 'materialize-css'; 6 | import 'angular2-materialize'; 7 | 8 | AnimationBuilder.defaults.fixed = true; 9 | -------------------------------------------------------------------------------- /src/js/components/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { AnimationService, AnimationBuilder } from 'css-animator'; 4 | import { Unsubscriber } from 'app/components'; 5 | import { LocaleService, PlaylistService } from 'app/services'; 6 | 7 | import template from './app.html'; 8 | 9 | @Component({ 10 | selector: 'quiz-app', 11 | template: template, 12 | providers: [ 13 | AnimationService, 14 | LocaleService, 15 | PlaylistService 16 | ] 17 | }) 18 | export class AppComponent implements OnInit { 19 | 20 | private _animator: AnimationBuilder; 21 | 22 | constructor(animationService: AnimationService) { 23 | this._animator = animationService.builder(); 24 | } 25 | 26 | public ngOnInit(): void { 27 | let loadingElem = document.getElementById('app-loading'); 28 | let spinningElem = loadingElem.querySelector('.loader'); 29 | 30 | this._animator 31 | .setDuration(600) 32 | .setType('fadeOut') 33 | .hide(loadingElem) 34 | .then(() => { 35 | spinningElem.classList.remove('running'); 36 | }); 37 | 38 | } 39 | 40 | } 41 | 42 | export default AppComponent; 43 | -------------------------------------------------------------------------------- /src/js/components/app/app.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/js/components/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unsubscriber'; 2 | export * from './app/index'; 3 | export * from './landing/index'; 4 | export * from './quiz/index'; 5 | -------------------------------------------------------------------------------- /src/js/components/landing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './landing.component'; 2 | export * from './region_options'; 3 | -------------------------------------------------------------------------------- /src/js/components/landing/landing.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, AfterViewInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AnimationService, AnimationBuilder } from 'css-animator'; 5 | import { LocaleService } from 'app/services'; 6 | import { RegionOptions, REGION_VALUES } from './region_options'; 7 | 8 | import template from './landing.html'; 9 | import mainStyle from './landing.css'; 10 | 11 | @Component({ 12 | selector: 'landing-host', 13 | host: { 14 | 'hidden': 'true' 15 | }, 16 | template: template, 17 | styles: [ 18 | mainStyle 19 | ] 20 | }) 21 | export class LandingComponent implements OnInit, AfterViewInit { 22 | 23 | public selectOptions: RegionOptions[] = REGION_VALUES; 24 | public submitted = false; 25 | 26 | private _animator: AnimationBuilder; 27 | private _regionSelection: any; 28 | 29 | constructor( 30 | private _elementRef: ElementRef, 31 | private router: Router, 32 | private _localeService: LocaleService, 33 | animationService: AnimationService) { 34 | this._animator = animationService.builder(); 35 | } 36 | 37 | public ngOnInit() { 38 | this._regionSelection = this._localeService.locale.value; 39 | } 40 | 41 | public ngAfterViewInit() { 42 | this._animator 43 | .setType('fadeInUp') 44 | .setDelay(150) 45 | .setDuration(700) 46 | .show(this._elementRef.nativeElement); 47 | } 48 | 49 | get regionSelection() { 50 | return this._regionSelection; 51 | } 52 | 53 | set regionSelection(value) { 54 | let locale; 55 | 56 | for (let region of this.selectOptions) { 57 | if (region.value === value) { 58 | locale = region; 59 | } 60 | } 61 | 62 | this._localeService.locale = locale || value; 63 | this._regionSelection = value; 64 | } 65 | 66 | public startQuiz(): void { 67 | if (this.submitted) { 68 | return; 69 | } 70 | 71 | this.submitted = true; 72 | 73 | this._animator 74 | .setType('fadeOutDown') 75 | .setDelay(350) 76 | .setDuration(600) 77 | .hide(this._elementRef.nativeElement.firstChild) 78 | .then(() => { 79 | this.router.navigate(['/quiz']); 80 | }); 81 | } 82 | 83 | } 84 | 85 | export default LandingComponent; 86 | -------------------------------------------------------------------------------- /src/js/components/landing/landing.css: -------------------------------------------------------------------------------- 1 | .card { 2 | max-width: 800px; 3 | margin-left: auto; 4 | margin-right: auto; 5 | margin-top: 10rem; 6 | } 7 | 8 | @media only screen and (max-width: 992px) { 9 | .card { 10 | margin-top: 8rem; 11 | } 12 | } 13 | 14 | @media only screen and (max-width: 435px) { 15 | .card { 16 | margin-top: 6rem; 17 | } 18 | } 19 | 20 | @media only screen and (max-width: 375px) { 21 | .card { 22 | margin-top: 5rem; 23 | } 24 | } 25 | 26 | @media only screen and (max-width: 320px) { 27 | .card { 28 | margin-top: 3rem; 29 | } 30 | } 31 | 32 | .card blockquote { 33 | max-width: 33.5rem; 34 | margin: 15px auto 25px auto; 35 | line-height: 1.7; 36 | } 37 | 38 | .card blockquote a.btn-flat { 39 | margin: 0; 40 | height: 20px; 41 | padding: 0 5px; 42 | line-height: 20px; 43 | margin-top: -2px; 44 | } 45 | 46 | a.btn { 47 | margin-top: 20px; 48 | margin-bottom: 20px; 49 | } 50 | -------------------------------------------------------------------------------- /src/js/components/landing/landing.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | You're almost ready to start! 6 |
7 | This is a music quiz, realized with Angular 8 | using data from the Spotify API, 9 | developed with in Austria and available on GitHub. 10 |
11 |
12 | 15 | 16 |
17 |

18 | Start Quiz 19 |

20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/js/components/landing/region_options.ts: -------------------------------------------------------------------------------- 1 | export interface RegionOptions { 2 | [key: string]: string; 3 | name: string; 4 | value: string; 5 | } 6 | 7 | export const REGION_VALUES: RegionOptions[] = [ 8 | { 9 | name: 'International', 10 | value: '' 11 | }, 12 | { 13 | name: 'Andora', 14 | value: 'ad' 15 | }, 16 | { 17 | name: 'Argentina', 18 | value: 'ar' 19 | }, 20 | { 21 | name: 'Australia', 22 | value: 'au' 23 | }, 24 | { 25 | name: 'Austria', 26 | value: 'at' 27 | }, 28 | { 29 | name: 'Belgium', 30 | value: 'be' 31 | }, 32 | { 33 | name: 'Bolivia', 34 | value: 'bo' 35 | }, 36 | { 37 | name: 'Brazil', 38 | value: 'br' 39 | }, 40 | { 41 | name: 'Bulgaria', 42 | value: 'bg' 43 | }, 44 | { 45 | name: 'Canada', 46 | value: 'ca' 47 | }, 48 | { 49 | name: 'Chile', 50 | value: 'cl' 51 | }, 52 | { 53 | name: 'Columbia', 54 | value: 'cr' 55 | }, 56 | { 57 | name: 'Costa Rica', 58 | value: 'co' 59 | }, 60 | { 61 | name: 'Cyprus', 62 | value: 'cy' 63 | }, 64 | { 65 | name: 'Czech Republic', 66 | value: 'cz' 67 | }, 68 | { 69 | name: 'Dominican Republic', 70 | value: 'do' 71 | }, 72 | { 73 | name: 'Denmark', 74 | value: 'dk' 75 | }, 76 | { 77 | name: 'Ecuador', 78 | value: 'ec' 79 | }, 80 | { 81 | name: 'El Salvador', 82 | value: 'sv' 83 | }, 84 | { 85 | name: 'Estonia', 86 | value: 'ee' 87 | }, 88 | { 89 | name: 'Finland', 90 | value: 'fi' 91 | }, 92 | { 93 | name: 'France', 94 | value: 'fr' 95 | }, 96 | { 97 | name: 'Germany', 98 | value: 'de' 99 | }, 100 | { 101 | name: 'Greece', 102 | value: 'gr' 103 | }, 104 | { 105 | name: 'Guatemala', 106 | value: 'gt' 107 | }, 108 | { 109 | name: 'Honduras', 110 | value: 'hn' 111 | }, 112 | { 113 | name: 'Hong Kong', 114 | value: 'hk' 115 | }, 116 | { 117 | name: 'Hungary', 118 | value: 'hu' 119 | }, 120 | { 121 | name: 'Iceland', 122 | value: 'id' 123 | }, 124 | { 125 | name: 'Indonesia', 126 | value: 'is' 127 | }, 128 | { 129 | name: 'Ireland', 130 | value: 'ie' 131 | }, 132 | { 133 | name: 'Italy', 134 | value: 'it' 135 | }, 136 | { 137 | name: 'Japan', 138 | value: 'jp' 139 | }, 140 | { 141 | name: 'Latvia', 142 | value: 'lv' 143 | }, 144 | { 145 | name: 'Liechtenstein', 146 | value: 'li' 147 | }, 148 | { 149 | name: 'Lithuania', 150 | value: 'lt' 151 | }, 152 | { 153 | name: 'Luxembourg', 154 | value: 'lu' 155 | }, 156 | { 157 | name: 'Malaysia', 158 | value: 'my' 159 | }, 160 | { 161 | name: 'Malta', 162 | value: 'mt' 163 | }, 164 | { 165 | name: 'Monaco', 166 | value: 'mc' 167 | }, 168 | { 169 | name: 'Mexico', 170 | value: 'mx' 171 | }, 172 | { 173 | name: 'Netherlands', 174 | value: 'nl' 175 | }, 176 | { 177 | name: 'New Zealand', 178 | value: 'nz' 179 | }, 180 | { 181 | name: 'Nicaragua', 182 | value: 'ni' 183 | }, 184 | { 185 | name: 'Norway', 186 | value: 'no' 187 | }, 188 | { 189 | name: 'Panama', 190 | value: 'pa' 191 | }, 192 | { 193 | name: 'Paraguay', 194 | value: 'py' 195 | }, 196 | { 197 | name: 'Peru', 198 | value: 'pe' 199 | }, 200 | { 201 | name: 'Philippines', 202 | value: 'ph' 203 | }, 204 | { 205 | name: 'Poland', 206 | value: 'pl' 207 | }, 208 | { 209 | name: 'Portugal', 210 | value: 'pt' 211 | }, 212 | { 213 | name: 'Spain', 214 | value: 'es' 215 | }, 216 | { 217 | name: 'Switzerland', 218 | value: 'ch' 219 | }, 220 | { 221 | name: 'Singapore', 222 | value: 'sg' 223 | }, 224 | { 225 | name: 'Slovakia', 226 | value: 'sk' 227 | }, 228 | { 229 | name: 'Sweden', 230 | value: 'se' 231 | }, 232 | { 233 | name: 'Taiwan', 234 | value: 'tw' 235 | }, 236 | { 237 | name: 'Turkey', 238 | value: 'tr' 239 | }, 240 | { 241 | name: 'United Kingdom', 242 | value: 'gb' 243 | }, 244 | { 245 | name: 'USA', 246 | value: 'us' 247 | }, 248 | { 249 | name: 'Uruguay', 250 | value: 'uy' 251 | } 252 | ]; 253 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/answers.ts: -------------------------------------------------------------------------------- 1 | export * from './choice-album/index'; 2 | export * from './choice-artist/index'; 3 | export * from './choice-track/index'; 4 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-album/answer-choice-album.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { QuizService } from 'app/services'; 4 | import { GenericAnswer } from 'app/components'; 5 | import { shuffle, randomTracksExcluding } from 'app/helpers'; 6 | 7 | import template from './answer-choice-album.html'; 8 | import mainStyle from './answer-choice-album.css'; 9 | import commonStyle from '../common.css'; 10 | 11 | @Component({ 12 | selector: 'answer-choice-album', 13 | template: template, 14 | styles: [ 15 | commonStyle, 16 | mainStyle 17 | ] 18 | }) 19 | export class AnswerChoiceAlbumComponent extends GenericAnswer { 20 | 21 | public answers: { title: string, correct: boolean }[] = []; 22 | 23 | constructor(private quizService: QuizService) { 24 | super(); 25 | } 26 | 27 | protected init(): void { 28 | const answers = [{ title: this.question.track.album.name, correct: true }]; 29 | 30 | for (let randomTrack of randomTracksExcluding(this.question.track.id, 3, this.quizService.tracks)) { 31 | answers.push({ 32 | title: randomTrack.album.name, 33 | correct: false 34 | }); 35 | } 36 | 37 | shuffle(answers); 38 | this.answers = answers; 39 | } 40 | 41 | } 42 | 43 | export default AnswerChoiceAlbumComponent; 44 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-album/answer-choice-album.css: -------------------------------------------------------------------------------- 1 | @media only screen and (min-width: 992px) { 2 | .row.answers .col:nth-child(even) { 3 | text-align: right; 4 | padding-right: 15%; 5 | direction: rtl; 6 | } 7 | .row.answers .col:nth-child(even) [type="checkbox"]+label { 8 | padding-right: 35px; 9 | background-color: #fff; 10 | } 11 | .row.answers .col:nth-child(even) [type="checkbox"]+label:before, .row.answers .col:nth-child(even) [type="checkbox"]:not(.filled-in)+label:after { 12 | left: auto; 13 | right: 0; 14 | } 15 | .row.answers .col:nth-child(even) [type="checkbox"]:checked+label:before { 16 | right: 10px !important; 17 | } 18 | .row.answers .col:nth-child(odd) { 19 | text-align: left; 20 | padding-left: 12%; 21 | } 22 | .row.answers .col { 23 | white-space: nowrap; 24 | overflow: visible; 25 | text-overflow: ellipsis; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-album/answer-choice-album.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-album/index.ts: -------------------------------------------------------------------------------- 1 | export * from './answer-choice-album.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-artist/answer-choice-artist.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { QuizService } from 'app/services'; 4 | import { GenericAnswer } from 'app/components'; 5 | import { shuffle, randomTracksExcluding } from 'app/helpers'; 6 | 7 | import template from './answer-choice-artist.html'; 8 | import mainStyle from './answer-choice-artist.css'; 9 | import commonStyle from '../common.css'; 10 | 11 | @Component({ 12 | selector: 'answer-choice-artist', 13 | template: template, 14 | styles: [ 15 | commonStyle, 16 | mainStyle 17 | ] 18 | }) 19 | export class AnswerChoiceArtistComponent extends GenericAnswer { 20 | 21 | public answers: { title: string, correct: boolean }[] = []; 22 | 23 | constructor(private quizService: QuizService) { 24 | super(); 25 | } 26 | 27 | protected init(): void { 28 | const answers = [{ title: this.question.track.artists[0].name, correct: true }]; 29 | 30 | for (let randomTrack of randomTracksExcluding(this.question.track.id, 3, this.quizService.tracks)) { 31 | answers.push({ 32 | title: randomTrack.artists[0].name, 33 | correct: false 34 | }); 35 | } 36 | 37 | shuffle(answers); 38 | this.answers = answers; 39 | } 40 | 41 | } 42 | 43 | export default AnswerChoiceArtistComponent; 44 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-artist/answer-choice-artist.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/js/components/quiz/answers/choice-artist/answer-choice-artist.css -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-artist/answer-choice-artist.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-artist/index.ts: -------------------------------------------------------------------------------- 1 | export * from './answer-choice-artist.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-track/answer-choice-track.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { QuizService } from 'app/services'; 4 | import { GenericAnswer } from 'app/components'; 5 | import { shuffle, randomTracksExcluding } from 'app/helpers'; 6 | 7 | import template from './answer-choice-track.html'; 8 | import mainStyle from './answer-choice-track.css'; 9 | import commonStyle from '../common.css'; 10 | 11 | @Component({ 12 | selector: 'answer-choice-track', 13 | template: template, 14 | styles: [ 15 | commonStyle, 16 | mainStyle 17 | ] 18 | }) 19 | export class AnswerChoiceTrackComponent extends GenericAnswer { 20 | 21 | public answers: { title: string, correct: boolean }[] = []; 22 | 23 | constructor(private quizService: QuizService) { 24 | super(); 25 | } 26 | 27 | protected init(): void { 28 | const answers = [{ title: this.question.track.name, correct: true }]; 29 | 30 | for (let randomTrack of randomTracksExcluding(this.question.track.id, 3, this.quizService.tracks)) { 31 | answers.push({ 32 | title: randomTrack.name, 33 | correct: false 34 | }); 35 | } 36 | 37 | shuffle(answers); 38 | this.answers = answers; 39 | } 40 | 41 | } 42 | 43 | export default AnswerChoiceTrackComponent; 44 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-track/answer-choice-track.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/js/components/quiz/answers/choice-track/answer-choice-track.css -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-track/answer-choice-track.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/choice-track/index.ts: -------------------------------------------------------------------------------- 1 | export * from './answer-choice-track.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/common.css: -------------------------------------------------------------------------------- 1 | .answers { 2 | padding-bottom: 4.5em; 3 | } 4 | 5 | .row.answers { 6 | line-height: 4; 7 | margin-top: 2em; 8 | margin-bottom: 2em; 9 | } 10 | 11 | @media only screen and (min-width: 992px) { 12 | .row.answers .col:nth-child(even) { 13 | text-align: right; 14 | padding-right: 15%; 15 | direction: rtl; 16 | } 17 | .row.answers .col:nth-child(even) [type="checkbox"]+label { 18 | padding-right: 35px; 19 | background-color: #fff; 20 | } 21 | .row.answers .col:nth-child(even) [type="checkbox"]+label:before, .row.answers .col:nth-child(even) [type="checkbox"]:not(.filled-in)+label:after { 22 | left: auto; 23 | right: 0; 24 | } 25 | .row.answers .col:nth-child(even) [type="checkbox"]:checked+label:before { 26 | right: 10px !important; 27 | } 28 | .row.answers .col:nth-child(odd) { 29 | text-align: left; 30 | padding-left: 12%; 31 | } 32 | .row.answers .col { 33 | white-space: nowrap; 34 | overflow: visible; 35 | text-overflow: ellipsis; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/generic_answer.ts: -------------------------------------------------------------------------------- 1 | import { Input, Output, EventEmitter, OnInit } from '@angular/core'; 2 | import { Question } from 'app/contracts'; 3 | 4 | export abstract class GenericAnswer implements OnInit { 5 | 6 | @Input() public question: Question; 7 | @Output() public onAnswerChange: EventEmitter = new EventEmitter(); 8 | 9 | public ngOnInit(): void { 10 | this.init(); 11 | } 12 | 13 | public answerChanged(answer: any | null): void { 14 | this.onAnswerChange.emit(answer); 15 | } 16 | 17 | public hasAnswer(): boolean { 18 | return this.question.answer !== undefined; 19 | } 20 | 21 | protected abstract init(): void; 22 | 23 | } 24 | 25 | export default GenericAnswer; 26 | -------------------------------------------------------------------------------- /src/js/components/quiz/answers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generic_answer'; 2 | export * from './answers'; 3 | 4 | import { importToArray } from 'app/helpers'; 5 | import * as answers from './answers'; 6 | 7 | const ANSWER_COMPONENTS = importToArray(answers); 8 | 9 | export { ANSWER_COMPONENTS }; 10 | -------------------------------------------------------------------------------- /src/js/components/quiz/done/index.ts: -------------------------------------------------------------------------------- 1 | export * from './quiz-done.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/done/quiz-done.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef } from '@angular/core'; 2 | 3 | import { AnimationService, AnimationBuilder } from 'css-animator'; 4 | 5 | import { Unsubscriber } from 'app/components'; 6 | import { QuizService } from 'app/services'; 7 | 8 | import template from './quiz-done.html'; 9 | import mainStyle from './quiz-done.css'; 10 | 11 | @Component({ 12 | selector: 'quiz-done', 13 | template: template, 14 | styles: [ 15 | mainStyle 16 | ], 17 | host: { 18 | hidden: 'true' 19 | } 20 | }) 21 | export class QuizDoneComponent extends Unsubscriber { 22 | 23 | private _animator: AnimationBuilder; 24 | private _hidden: boolean = true; 25 | 26 | constructor( 27 | private _quizService: QuizService, 28 | private _elementRef: ElementRef, 29 | animationService: AnimationService) { 30 | super(); 31 | this._animator = animationService.builder(); 32 | this.subscribe(); 33 | } 34 | 35 | public show(): void { 36 | if (!this._hidden) { 37 | return; 38 | } 39 | 40 | this._hidden = false; 41 | 42 | this._animator 43 | .setType('fadeInUp') 44 | .setDelay(400) 45 | .setDuration(600) 46 | .show(this._elementRef.nativeElement); 47 | } 48 | 49 | public hide(): void { 50 | if (this._hidden) { 51 | return; 52 | } 53 | 54 | this._hidden = true; 55 | 56 | this._animator.setType('fadeOutDown') 57 | .setDelay(100) 58 | .setDuration(600) 59 | .hide(this._elementRef.nativeElement); 60 | } 61 | 62 | private subscribe(): void { 63 | let onCompleted = this._quizService 64 | .onCompleted 65 | .subscribe(() => { 66 | this.show(); 67 | }); 68 | 69 | let onRefresh = this._quizService 70 | .onRefresh 71 | .subscribe(() => { 72 | this.hide(); 73 | }); 74 | 75 | let onClose = this._quizService 76 | .onClose 77 | .subscribe(() => { 78 | this.hide(); 79 | }); 80 | 81 | this.subscriptions.push(onCompleted); 82 | this.subscriptions.push(onRefresh); 83 | this.subscriptions.push(onClose); 84 | } 85 | 86 | } 87 | 88 | export default QuizDoneComponent; 89 | -------------------------------------------------------------------------------- /src/js/components/quiz/done/quiz-done.css: -------------------------------------------------------------------------------- 1 | .done-icon { 2 | margin-top: 20px; 3 | margin-bottom: 25px; 4 | } 5 | 6 | .done-icon .circle { 7 | width: 130px; 8 | height: 130px; 9 | display: inline-block; 10 | } 11 | 12 | .done-icon .circle img { 13 | width: 92px; 14 | margin-top: 16px; 15 | position: relative; 16 | } 17 | 18 | .card-content .text { 19 | display: table; 20 | width: 100%; 21 | margin-bottom: 30px; 22 | } 23 | 24 | .card-content .text p { 25 | vertical-align: middle; 26 | display: table-cell; 27 | text-align: center; 28 | } 29 | 30 | .card-content .text p i { 31 | display: inline-block; 32 | padding: 5px; 33 | font-size: 20px; 34 | margin: 0 5px; 35 | vertical-align: middle; 36 | } 37 | -------------------------------------------------------------------------------- /src/js/components/quiz/done/quiz-done.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
Congrats, you have finished the quiz!
7 |
8 |
9 | 10 |
11 |
12 |
13 |

14 | Click the subject icon, to display all questions and your answers. 15 |

16 | You can also take another quiz by clicking refresh, 17 | or start over with close. 18 |

19 |
20 |
21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/js/components/quiz/index.ts: -------------------------------------------------------------------------------- 1 | export * from './answers/index'; 2 | export * from './done/index'; 3 | export * from './nav/index'; 4 | export * from './questions/index'; 5 | export * from './status/index'; 6 | export * from './quiz.component'; 7 | -------------------------------------------------------------------------------- /src/js/components/quiz/nav/index.ts: -------------------------------------------------------------------------------- 1 | export * from './quiz-nav.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/nav/quiz-nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | import { AnimatesDirective } from 'css-animator'; 3 | 4 | import { QuizService } from 'app/services'; 5 | 6 | import template from './quiz-nav.html'; 7 | import mainStyle from './quiz-nav.css'; 8 | 9 | @Component({ 10 | selector: 'quiz-nav', 11 | template: template, 12 | styles: [ 13 | mainStyle 14 | ] 15 | }) 16 | export class QuizNavComponent { 17 | @Output() public onGoHome = new EventEmitter(); 18 | @Output() public onRefresh = new EventEmitter(); 19 | @Output() public onClose = new EventEmitter(); 20 | 21 | constructor(private _quizService: QuizService) { 22 | 23 | } 24 | 25 | public get progress(): number { 26 | return this._quizService.progress(); 27 | } 28 | 29 | public goHome(): void { 30 | this.onGoHome.emit(); 31 | } 32 | 33 | public refresh(navAnimatesDirective: AnimatesDirective): void { 34 | this.onRefresh.emit(); 35 | } 36 | 37 | public close(navAnimatesDirective: AnimatesDirective): void { 38 | this.onClose.emit(); 39 | navAnimatesDirective.hide({ type: 'fadeOutUp', delay: 400, duration: 600 }); 40 | } 41 | 42 | } 43 | 44 | export default QuizNavComponent; 45 | -------------------------------------------------------------------------------- /src/js/components/quiz/nav/quiz-nav.css: -------------------------------------------------------------------------------- 1 | .progress { 2 | position: absolute; 3 | border-radius: 0; 4 | margin: 0; 5 | background-color: transparent; 6 | bottom: 0; 7 | } 8 | 9 | .nav-wrapper { 10 | margin-bottom: 3rem; 11 | } 12 | 13 | .menuitem { 14 | cursor: pointer; 15 | } 16 | 17 | ul li .btn i { 18 | font-size: 1.3rem; 19 | line-height: 36px; 20 | } 21 | 22 | ul li a.menuitem:hover { 23 | background-color: rgba(236, 239, 241, 1); 24 | } 25 | 26 | ul li a.btn { 27 | padding: 0 1.8rem 0 1.3rem; 28 | } 29 | 30 | .brand-logo { 31 | height: 100%; 32 | } 33 | 34 | a.brand-logo { 35 | cursor: default; 36 | } 37 | 38 | .brand-logo img { 39 | cursor: default; 40 | height: 1.3em; 41 | margin-left: 10px; 42 | display: block; 43 | position: absolute; 44 | top: 50%; 45 | margin-top: -0.7em; 46 | } 47 | 48 | @media only screen and (max-width: 369px) { 49 | .github-button { 50 | display: none; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/js/components/quiz/nav/quiz-nav.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/album/index.ts: -------------------------------------------------------------------------------- 1 | export * from './question-album.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/album/question-album.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { QuizService } from 'app/services'; 4 | import { GenericQuestion, QuestionType } from 'app/components'; 5 | 6 | import template from './question-album.html'; 7 | import mainStyle from './question-album.css'; 8 | import commonStyle from '../common.css'; 9 | 10 | @Component({ 11 | selector: 'question-album', 12 | template: template, 13 | styles: [ 14 | commonStyle, 15 | mainStyle 16 | ] 17 | }) 18 | export class QuestionAlbumComponent extends GenericQuestion { 19 | 20 | public static type = QuestionType.Album; 21 | 22 | public init(): void { 23 | this.setTitle('Who is the artist of this album?'); 24 | this.setCorrectAnswer(this.question.track.artists[0].name); 25 | } 26 | 27 | } 28 | 29 | export default QuestionAlbumComponent; 30 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/album/question-album.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/js/components/quiz/questions/album/question-album.css -------------------------------------------------------------------------------- /src/js/components/quiz/questions/album/question-album.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | album 6 |
7 |
8 | {{ question.title }} 9 |
10 | 11 | {{ question.track.album.name }} 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | album 22 | 23 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/audio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './question-audio.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/audio/question-audio.component.ts: -------------------------------------------------------------------------------- 1 | import { interval } from 'rxjs'; 2 | import { timeInterval, take } from 'rxjs/operators'; 3 | 4 | import { Component } from '@angular/core'; 5 | 6 | import { GenericQuestion, QuestionType } from 'app/components'; 7 | import { QuizService } from 'app/services'; 8 | 9 | import template from './question-audio.html'; 10 | import mainStyle from './question-audio.css'; 11 | import commonStyle from '../common.css'; 12 | 13 | type Player = { player: HTMLAudioElement, isPlaying?: boolean }; 14 | 15 | @Component({ 16 | selector: 'question-audio', 17 | template: template, 18 | styles: [ 19 | commonStyle, 20 | mainStyle 21 | ] 22 | }) 23 | export class QuestionAudioComponent extends GenericQuestion { 24 | 25 | public static type = QuestionType.Audio; 26 | 27 | private static _players: Player[] = []; 28 | 29 | private _player: HTMLAudioElement; 30 | private _countdown = 10; 31 | 32 | constructor(private _quizService: QuizService) { 33 | super(); 34 | } 35 | 36 | public init(): void { 37 | this.setTitle('What is the name of this song?'); 38 | this.setDescription(`You can play ${this._countdown} seconds of the song.`); 39 | this.setCorrectAnswer(this.question.track.name); 40 | this.subscribeToClose(); 41 | this.subscribeToRefresh(); 42 | } 43 | 44 | public activate(): void { 45 | super.activate(); 46 | } 47 | 48 | public deactivate(): void { 49 | this.stopAllPlayers(); 50 | 51 | if (this._player) { 52 | this._player.pause(); 53 | } 54 | 55 | super.deactivate(); 56 | } 57 | 58 | public playSong(player: HTMLAudioElement, button: HTMLElement): void { 59 | this.stopAllPlayers(); 60 | 61 | this._player = player; 62 | let countdown = this._countdown - 1; 63 | 64 | interval(1000) 65 | .pipe( 66 | timeInterval(), 67 | take(this._countdown) 68 | ) 69 | .subscribe((next) => { 70 | this._countdown = countdown - next.value; 71 | }, (error) => { 72 | 73 | }, () => { 74 | this._countdown = 0; 75 | player.pause(); 76 | }); 77 | 78 | player.play(); 79 | button.setAttribute('disabled', ''); 80 | button.classList.add('disabled'); 81 | } 82 | 83 | public stopPlayback(player: HTMLAudioElement): void { 84 | let p = this.getPlayer(player); 85 | 86 | p.isPlaying = false; 87 | player.currentTime = 0; 88 | player.pause(); 89 | } 90 | 91 | public stopAllPlayers(): void { 92 | for (let p of QuestionAudioComponent._players) { 93 | this.stopPlayback(p.player); 94 | } 95 | } 96 | 97 | public startPlayback(player: HTMLAudioElement): void { 98 | let p = this.getPlayer(player); 99 | this.stopAllPlayers(); 100 | 101 | p.isPlaying = true; 102 | player.currentTime = 0; 103 | player.play(); 104 | } 105 | 106 | public togglePlayback(player: HTMLAudioElement): void { 107 | if (player.ended || player.paused) { 108 | this.startPlayback(player); 109 | return; 110 | } 111 | 112 | this.stopPlayback(player); 113 | } 114 | 115 | public isAudioPlaying(player: HTMLAudioElement): boolean { 116 | let p = this.getPlayer(player); 117 | 118 | if (p.isPlaying) { 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | 125 | public getPlayer(player: HTMLAudioElement): Player { 126 | for (let p of QuestionAudioComponent._players) { 127 | if (p.player === player) { 128 | return p; 129 | } 130 | } 131 | 132 | let entry = { player: player }; 133 | QuestionAudioComponent._players.push(entry); 134 | 135 | return entry; 136 | } 137 | 138 | get countdown(): number { 139 | return this._countdown; 140 | } 141 | 142 | private subscribeToClose(): void { 143 | let subscription = this._quizService 144 | .onClose 145 | .subscribe(() => { 146 | QuestionAudioComponent._players = []; 147 | }); 148 | 149 | this.subscriptions.push(subscription); 150 | } 151 | 152 | private subscribeToRefresh(): void { 153 | let subscription = this._quizService 154 | .onRefresh 155 | .subscribe(() => { 156 | QuestionAudioComponent._players = []; 157 | }); 158 | 159 | this.subscriptions.push(subscription); 160 | } 161 | 162 | } 163 | 164 | export default QuestionAudioComponent; 165 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/audio/question-audio.css: -------------------------------------------------------------------------------- 1 | audio { 2 | display: none; 3 | } 4 | 5 | .song-preview { 6 | margin-top: 10px; 7 | } 8 | 9 | .song-preview button { 10 | padding: 0 1rem; 11 | } 12 | 13 | @media only screen and (min-width: 992px) { 14 | .song-preview button { 15 | padding: 0 15px; 16 | } 17 | .song-preview button { 18 | padding: 0 2rem; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/audio/question-audio.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | hearing 6 |
7 |
8 | {{ title }} 9 |
10 |
11 | 14 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 29 | {{ !isAudioPlaying(player) ? 'play_arrow' : 'stop' }} 30 | 31 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/common.css: -------------------------------------------------------------------------------- 1 | .card-panel { 2 | margin-top: 1.5em; 3 | } 4 | 5 | .card-panel button i { 6 | line-height: 15px 7 | } 8 | 9 | .card-panel i { 10 | padding: 12px; 11 | font-size: 24px; 12 | } 13 | 14 | .card-panel .row.valign-wrapper { 15 | margin-bottom: 0; 16 | } 17 | 18 | .question-title { 19 | padding-left: 1.5rem; 20 | } 21 | 22 | @media only screen and (min-width: 992px) { 23 | .question-title { 24 | padding-left: 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/generic_question.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, ViewChild, TemplateRef, AfterViewInit } from '@angular/core'; 2 | import { Unsubscriber } from 'app/components'; 3 | import { Question } from 'app/contracts'; 4 | 5 | export abstract class GenericQuestion extends Unsubscriber implements AfterViewInit { 6 | @ViewChild('statusTemplate') public statusTemplate: TemplateRef; 7 | 8 | public answerReceived: EventEmitter = new EventEmitter(); 9 | public answerRemoved: EventEmitter = new EventEmitter(); 10 | 11 | private _question: Question; 12 | private _title: string; 13 | private _description: string; 14 | 15 | public abstract init(): void; 16 | 17 | public ngAfterViewInit(): void { 18 | this._question.statusTemplate = this.statusTemplate; 19 | } 20 | 21 | public activate(): void { 22 | 23 | } 24 | 25 | public deactivate(): void { 26 | this.question.answered = true; 27 | this.question.wasCorrect = this.isCorrect(); 28 | } 29 | 30 | public hasAnswer(): boolean { 31 | return this.question.answer !== undefined; 32 | } 33 | 34 | public isCorrect(): boolean { 35 | return this.hasAnswer() && this.question.correctAnswer === this.question.answer; 36 | } 37 | 38 | public answerChanged(answer: any | null): void { 39 | if (answer === null) { 40 | this.setAnswer(void 0); 41 | this.answerRemoved.emit(); 42 | return; 43 | } 44 | 45 | this.setAnswer(answer); 46 | this.answerReceived.emit(); 47 | } 48 | 49 | public setCorrectAnswer(answer: any): void { 50 | this.question.correctAnswer = answer; 51 | } 52 | 53 | public setAnswer(answer: any): void { 54 | this.question.answer = answer; 55 | } 56 | 57 | public setTitle(title: string): void { 58 | this._question.title = title; 59 | this._title = title; 60 | } 61 | 62 | public setDescription(desciption: string): void { 63 | this._question.description = desciption; 64 | this._description = desciption; 65 | } 66 | 67 | get question(): Question { 68 | return this._question; 69 | } 70 | 71 | set question(question: Question) { 72 | question.component = this; 73 | this._question = question; 74 | } 75 | 76 | get title(): string { 77 | return this._title; 78 | } 79 | 80 | get desciption(): string { 81 | return this._description; 82 | } 83 | } 84 | 85 | export default GenericQuestion; 86 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/image/index.ts: -------------------------------------------------------------------------------- 1 | export * from './question-image.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/image/question-image.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { GenericQuestion, QuestionType } from 'app/components'; 4 | 5 | import template from './question-image.html'; 6 | import mainStyle from './question-image.css'; 7 | import commonStyle from '../common.css'; 8 | 9 | @Component({ 10 | selector: 'question-image', 11 | template: template, 12 | styles: [ 13 | commonStyle, 14 | mainStyle 15 | ] 16 | }) 17 | export class QuestionImageComponent extends GenericQuestion { 18 | 19 | public static type = QuestionType.Image; 20 | 21 | public init(): void { 22 | this.setTitle('What is the name of this album?'); 23 | this.setCorrectAnswer(this.question.track.album.name); 24 | } 25 | 26 | } 27 | 28 | export default QuestionImageComponent; 29 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/image/question-image.css: -------------------------------------------------------------------------------- 1 | .album-cover { 2 | width: 160px; 3 | height: 160px; 4 | margin: 0 auto; 5 | margin-top: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/image/question-image.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{ title }} 6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generic_question'; 2 | export * from './question.component'; 3 | export * from './questions'; 4 | export * from './types'; 5 | 6 | import { importToArray } from 'app/helpers'; 7 | import * as questions from './questions'; 8 | 9 | const QUESTION_COMPONENTS = importToArray(questions); 10 | const QUESTIONS_COMPONENTS_MAP = QUESTION_COMPONENTS.reduce((result, value, index, array) => { 11 | result[value.type] = value; 12 | return result; 13 | }, {}); 14 | 15 | export { QUESTION_COMPONENTS, QUESTIONS_COMPONENTS_MAP }; 16 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/question.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterViewInit, Input, ViewChild, ElementRef, ComponentFactoryResolver } from '@angular/core'; 2 | 3 | import { AnimationService, AnimationBuilder } from 'css-animator'; 4 | import { Unsubscriber, GenericQuestion, QUESTIONS_COMPONENTS_MAP } from 'app/components'; 5 | import { QuizService } from 'app/services'; 6 | import { Question } from 'app/contracts'; 7 | import { ContainerRefDirective } from 'app/directives'; 8 | 9 | import template from './question.html'; 10 | import mainStyle from './question.css'; 11 | 12 | @Component({ 13 | selector: 'question-host', 14 | template: template, 15 | styles: [ 16 | mainStyle 17 | ] 18 | }) 19 | export class QuestionComponent extends Unsubscriber implements AfterViewInit { 20 | @ViewChild(ContainerRefDirective) public questionHost: ContainerRefDirective; 21 | @Input() public question: Question; 22 | 23 | private _nextTimeout: any = null; 24 | private _animator: AnimationBuilder; 25 | private _active: boolean = false; 26 | 27 | private component: GenericQuestion; 28 | 29 | constructor(private _elementRef: ElementRef, private _quizService: QuizService, animationService: AnimationService, private resolver: ComponentFactoryResolver) { 30 | super(); 31 | this._animator = animationService.builder().setDuration(600); 32 | this.subscribeToActivate(); 33 | this.subscribeToClose(); 34 | } 35 | 36 | public ngAfterViewInit(): void { 37 | setTimeout(() => { 38 | this.loadComponent().then(() => { 39 | if (this.question.id === this._quizService.totalQuestions) { 40 | setTimeout(() => { 41 | this._quizService.ready(); 42 | }, 100); 43 | } 44 | }); 45 | }); 46 | } 47 | 48 | public answerIsCorrect(): boolean { 49 | if (!this.component) { 50 | return false; 51 | } 52 | 53 | return this.component.isCorrect(); 54 | } 55 | 56 | public nextQuestion(button: HTMLElement): void { 57 | if (this._nextTimeout !== null) { 58 | clearTimeout(this._nextTimeout); 59 | } 60 | 61 | if (!this.component || !this.component.hasAnswer()) { 62 | return; 63 | } 64 | 65 | if (this.answerIsCorrect()) { 66 | button.classList.add('green'); 67 | } else { 68 | button.classList.add('red'); 69 | } 70 | 71 | this._nextTimeout = setTimeout(() => { 72 | this._quizService.activateQuestion(this.question.id + 1); 73 | this._nextTimeout = null; 74 | }, 400); 75 | } 76 | 77 | get hasAnswer(): boolean { 78 | if (!this.component) { 79 | return false; 80 | } 81 | 82 | return this.component.hasAnswer(); 83 | } 84 | 85 | get active(): boolean { 86 | return this._active; 87 | } 88 | 89 | get quizService(): QuizService { 90 | return this._quizService; 91 | } 92 | 93 | private subscribeToActivate(): void { 94 | let subscription = this._quizService 95 | .onActivateQuestion.subscribe((questionNumber: number) => { 96 | if (questionNumber === this.question.id) { 97 | this.activateQuestion(); 98 | } else if (this.active) { 99 | this.deactivateQuestion(); 100 | } 101 | }); 102 | 103 | this.subscriptions.push(subscription); 104 | } 105 | 106 | private subscribeToClose(): void { 107 | let subscription = this._quizService 108 | .onClose 109 | .subscribe((questionNumber: number) => { 110 | if (this._active) { 111 | this._animator 112 | .setType('fadeOutDown') 113 | .setDuration(600) 114 | .hide(this._elementRef.nativeElement); 115 | } 116 | }); 117 | 118 | this.subscriptions.push(subscription); 119 | } 120 | 121 | private subscribeToAnswerReceived(): void { 122 | let subscription = this.component 123 | .answerReceived.subscribe(() => { 124 | 125 | }); 126 | 127 | this.subscriptions.push(subscription); 128 | } 129 | 130 | private subscribeToAnswerRemoved(): void { 131 | let subscription = this.component 132 | .answerRemoved.subscribe(() => { 133 | 134 | }); 135 | 136 | this.subscriptions.push(subscription); 137 | } 138 | 139 | private activateQuestion(): void { 140 | this.component.activate(); 141 | 142 | this._active = true; 143 | this._animator.setType('fadeInRight').setDelay(200); 144 | 145 | if (this.question.id === 1) { 146 | this._animator.setType('fadeInUp'); 147 | } 148 | 149 | this._animator.show(this._elementRef.nativeElement); 150 | } 151 | 152 | private deactivateQuestion(): void { 153 | this.component.deactivate(); 154 | 155 | this._animator.setType('fadeOutLeft').setDelay(0).setDuration(600); 156 | 157 | if (this.question.id === this._quizService.totalQuestions) { 158 | this._animator.setType('fadeOutDown'); 159 | this._quizService.completed(); 160 | } 161 | 162 | this._animator.hide(this._elementRef.nativeElement); 163 | this._active = false; 164 | } 165 | 166 | private async loadComponent(): Promise { 167 | // const componentFactory = this.resolver.resolveComponentFactory(QUESTIONS_COMPONENTS_MAP[0]); 168 | const componentFactory = this.resolver.resolveComponentFactory(QUESTIONS_COMPONENTS_MAP[this.question.type]); 169 | 170 | const viewContainerRef = this.questionHost.viewContainerRef; 171 | viewContainerRef.clear(); 172 | 173 | const componentRef = viewContainerRef.createComponent(componentFactory); 174 | const component = componentRef.instance as GenericQuestion; 175 | component.question = this.question; 176 | component.init(); 177 | 178 | this.component = component; 179 | } 180 | 181 | } 182 | 183 | export default QuestionComponent; 184 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/question.css: -------------------------------------------------------------------------------- 1 | .next { 2 | position: absolute; 3 | width: 200px; 4 | left: 50%; 5 | margin-left: -100px; 6 | margin-top: -5em; 7 | text-align: center; 8 | } 9 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/question.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | Question {{ question.id }} of {{ quizService.totalQuestions }} 7 | 8 | 9 | 10 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/questions.ts: -------------------------------------------------------------------------------- 1 | export * from './album/index'; 2 | export * from './audio/index'; 3 | export * from './image/index'; 4 | export * from './track/index'; 5 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/track/index.ts: -------------------------------------------------------------------------------- 1 | export * from './question-track.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/track/question-track.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { GenericQuestion, QuestionType } from 'app/components'; 4 | 5 | import template from './question-track.html'; 6 | import mainStyle from './question-track.css'; 7 | import commonStyle from '../common.css'; 8 | 9 | @Component({ 10 | selector: 'question-track', 11 | template: template, 12 | styles: [ 13 | commonStyle, 14 | mainStyle 15 | ] 16 | }) 17 | export class QuestionTrackComponent extends GenericQuestion { 18 | 19 | public static type = QuestionType.Track; 20 | 21 | public init(): void { 22 | this.setTitle('Who is the artist of this track?'); 23 | this.setCorrectAnswer(this.question.track.artists[0].name); 24 | } 25 | 26 | } 27 | 28 | export default QuestionTrackComponent; 29 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/track/question-track.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiandev/angular-quiz-app/6a919c783202306518b13f0682fd965dbc20b172/src/js/components/quiz/questions/track/question-track.css -------------------------------------------------------------------------------- /src/js/components/quiz/questions/track/question-track.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | audiotrack 6 |
7 |
8 | {{ question.title }} 9 |
10 | 11 | {{ question.track.name }} 12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | audiotrack 22 | 23 | -------------------------------------------------------------------------------- /src/js/components/quiz/questions/types.ts: -------------------------------------------------------------------------------- 1 | export enum QuestionType { 2 | Album, 3 | Audio, 4 | Image, 5 | Track 6 | } 7 | 8 | export default QuestionType; 9 | -------------------------------------------------------------------------------- /src/js/components/quiz/quiz.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AnimatesDirective } from 'css-animator'; 5 | 6 | import { Unsubscriber } from 'app/components'; 7 | import { QuizService } from 'app/services'; 8 | import { Question } from 'app/contracts'; 9 | 10 | import template from './quiz.html'; 11 | import mainStyle from './quiz.css'; 12 | 13 | @Component({ 14 | selector: 'quiz-host', 15 | template: template, 16 | styles: [ 17 | mainStyle 18 | ], 19 | providers: [ 20 | QuizService 21 | ] 22 | }) 23 | export class QuizComponent extends Unsubscriber implements OnInit { 24 | 25 | private _questions: Question[] = []; 26 | private _ready: boolean = false; 27 | 28 | constructor( 29 | private _router: Router, 30 | private _quizService: QuizService 31 | ) { 32 | super(); 33 | 34 | let onReady = this.quizService 35 | .onReady 36 | .subscribe(() => { 37 | this._ready = true; 38 | this.quizService.activateQuestion(1); 39 | }); 40 | 41 | let onRefresh = this.quizService 42 | .onRefresh 43 | .subscribe(() => { 44 | this._questions = this.quizService.questions; 45 | this._ready = true; 46 | }); 47 | 48 | this.subscriptions.push(onReady); 49 | this.subscriptions.push(onRefresh); 50 | } 51 | 52 | public ngOnInit(): void { 53 | this.quizService 54 | .init(10) 55 | .subscribe((questions) => { 56 | this._questions = this.quizService.questions; 57 | }); 58 | } 59 | 60 | public trackByQuestion(question: Question): number { 61 | return question.id; 62 | } 63 | 64 | public onGoHome(navAnimatesDirective: AnimatesDirective): void { 65 | setTimeout(() => { 66 | this._router.navigate(['/']); 67 | }, 1000); 68 | 69 | this.quizService.close(); 70 | } 71 | 72 | public onRefresh(navAnimatesDirective: AnimatesDirective): void { 73 | this.quizService.close(); 74 | 75 | setTimeout(() => { 76 | this._ready = false; 77 | setTimeout(() => { 78 | this.quizService.refresh(); 79 | }); 80 | }, 700); 81 | } 82 | 83 | public onClose(navAnimatesDirective: AnimatesDirective): void { 84 | this.onGoHome(navAnimatesDirective); 85 | } 86 | 87 | get questions(): Question[] { 88 | return this._questions; 89 | } 90 | 91 | get ready(): boolean { 92 | return this._ready; 93 | } 94 | 95 | get quizService(): QuizService { 96 | return this._quizService; 97 | } 98 | 99 | } 100 | 101 | export default QuizComponent; 102 | -------------------------------------------------------------------------------- /src/js/components/quiz/quiz.css: -------------------------------------------------------------------------------- 1 | .questions-loading { 2 | position: absolute; 3 | margin-top: 15rem; 4 | width: 60px; 5 | left: 50%; 6 | margin-left: -60px; 7 | } 8 | 9 | ul li .btn i { 10 | font-size: 1.3rem; 11 | line-height: 36px; 12 | } 13 | 14 | ul li a.menuitem:hover { 15 | background-color: rgba(236, 239, 241, 1); 16 | } 17 | 18 | ul li a.btn { 19 | padding: 0 1.8rem 0 1.3rem; 20 | } 21 | 22 | .brand-logo { 23 | height: 100%; 24 | } 25 | 26 | .brand-logo img { 27 | cursor: pointer; 28 | height: 1.3em; 29 | margin-left: 10px; 30 | display: block; 31 | position: absolute; 32 | top: 50%; 33 | margin-top: -0.7em; // put further to top than center 34 | } 35 | -------------------------------------------------------------------------------- /src/js/components/quiz/quiz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /src/js/components/quiz/status/index.ts: -------------------------------------------------------------------------------- 1 | export * from './quiz-status.component'; 2 | -------------------------------------------------------------------------------- /src/js/components/quiz/status/quiz-status.component.ts: -------------------------------------------------------------------------------- 1 | import IScroll from 'iscroll'; 2 | import { AfterContentInit, Component, ElementRef, EventEmitter, OnDestroy, Output } from '@angular/core'; 3 | import { MaterializeAction } from 'angular2-materialize'; 4 | 5 | import { Unsubscriber } from 'app/components'; 6 | import { QuizService } from 'app/services'; 7 | 8 | import template from './quiz-status.html'; 9 | import mainStyle from './quiz-status.css'; 10 | 11 | @Component({ 12 | selector: 'quiz-status', 13 | template: template, 14 | styles: [ 15 | mainStyle 16 | ] 17 | }) 18 | export class QuizStatusComponent extends Unsubscriber implements AfterContentInit, OnDestroy { 19 | 20 | @Output() public modalActions = new EventEmitter(); 21 | 22 | private _modal: HTMLElement = null; 23 | private _scroll: IScroll = null; 24 | 25 | constructor(private _quizService: QuizService, private _elementRef: ElementRef) { 26 | super(); 27 | } 28 | 29 | public ngAfterContentInit(): void { 30 | this._modal = this._elementRef 31 | .nativeElement 32 | .querySelector('#status'); 33 | 34 | this._scroll = new IScroll(this._modal, { 35 | deceleration: 0.005, 36 | mouseWheel: true, 37 | mouseWheelSpeed: 10, 38 | probeType: 2, 39 | tap: false, 40 | click: true 41 | }); 42 | 43 | let onActivateQuestion = this._quizService 44 | .onActivateQuestion 45 | .subscribe(() => { 46 | this.refreshScroll(); 47 | }); 48 | 49 | let onCompleted = this._quizService 50 | .onCompleted 51 | .subscribe(() => { 52 | this.refreshScroll(); 53 | }); 54 | 55 | this.subscriptions.push(onActivateQuestion); 56 | this.subscriptions.push(onCompleted); 57 | } 58 | 59 | public ngOnDestroy(): void { 60 | if (this._scroll) { 61 | this._scroll.destroy(); 62 | this._scroll = null; 63 | } 64 | } 65 | 66 | public refreshScroll(): void { 67 | this._modal.style.display = 'initial'; 68 | setTimeout(() => { 69 | this._modal.style.display = 'hidden'; 70 | this._scroll.refresh(); 71 | }); 72 | } 73 | 74 | public openModal(): void { 75 | this.modalActions.emit({ action: 'modal', params: ['open'] }); 76 | } 77 | 78 | public closeModal(): void { 79 | this.modalActions.emit({ action: 'modal', params: ['close'] }); 80 | } 81 | 82 | get quizService(): QuizService { 83 | return this._quizService; 84 | } 85 | 86 | } 87 | 88 | export default QuizStatusComponent; 89 | -------------------------------------------------------------------------------- /src/js/components/quiz/status/quiz-status.css: -------------------------------------------------------------------------------- 1 | :host { 2 | overflow-y: hidden; 3 | } 4 | 5 | .modal { 6 | overflow-y: hidden; 7 | } 8 | 9 | .modal-content { 10 | z-index: 5003; 11 | } 12 | 13 | .modal.bottom-sheet { 14 | max-height: 100%; 15 | } 16 | 17 | @media (min-height: 739px) { 18 | div.modal.bottom-sheet { 19 | max-height: 60%; 20 | } 21 | } 22 | 23 | .modal.bottom-sheet .modal-footer { 24 | display: none; 25 | } 26 | 27 | .modal.bottom-sheet.open .modal-footer { 28 | display: initial; 29 | } 30 | 31 | audio { 32 | display: none; 33 | } 34 | 35 | .play-button { 36 | cursor: pointer; 37 | } 38 | 39 | .done-button { 40 | z-index: 9003; 41 | position: fixed; 42 | right: 0; 43 | bottom: 0; 44 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); 45 | border-top: 1px solid rgba(96, 125, 139, 0.45); 46 | } 47 | 48 | .collection .collection-item.avatar { 49 | min-height: 65px; 50 | } 51 | 52 | .spacing { 53 | padding: 4px 6px; 54 | height: 40px; 55 | width: 100%; 56 | } 57 | -------------------------------------------------------------------------------- /src/js/components/quiz/status/quiz-status.html: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /src/js/components/unsubscriber.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'rxjs'; 2 | 3 | export class Unsubscriber { 4 | 5 | protected _subscriptions: Subscription[] = []; 6 | 7 | public constructor() { 8 | let destroy = (this as any).ngOnDestroy; 9 | 10 | (this as any).ngOnDestroy = function() { 11 | if (destroy) { 12 | destroy.bind(this)(); 13 | } 14 | 15 | this.unsubscribe(); 16 | }; 17 | } 18 | 19 | protected unsubscribe(): void { 20 | for (let i = 0, len = this._subscriptions.length; i < len; i++) { 21 | this._subscriptions[i].unsubscribe(); 22 | } 23 | 24 | this._subscriptions = []; 25 | } 26 | 27 | get subscriptions(): Subscription[] { 28 | return this._subscriptions; 29 | } 30 | 31 | } 32 | 33 | export default Unsubscriber; 34 | -------------------------------------------------------------------------------- /src/js/contracts/album.ts: -------------------------------------------------------------------------------- 1 | import Image from './image'; 2 | 3 | export interface Album { 4 | album_type: string; 5 | available_markets: string[]; 6 | external_urls: { 7 | spotify: string; 8 | }; 9 | href: string; 10 | id: string; 11 | images: Image[]; 12 | name: string; 13 | type: string; 14 | uri: string; 15 | } 16 | 17 | export default Album; 18 | -------------------------------------------------------------------------------- /src/js/contracts/artist.ts: -------------------------------------------------------------------------------- 1 | export interface Artist { 2 | external_urls: { 3 | spotify: string; 4 | }; 5 | href: string; 6 | id: string; 7 | name: string; 8 | type: string; 9 | uri: string; 10 | } 11 | 12 | export default Artist; 13 | -------------------------------------------------------------------------------- /src/js/contracts/image.ts: -------------------------------------------------------------------------------- 1 | export interface Image { 2 | width: number; 3 | height: number; 4 | url: string; 5 | } 6 | 7 | export default Image; 8 | -------------------------------------------------------------------------------- /src/js/contracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './album'; 2 | export * from './artist'; 3 | export * from './image'; 4 | export * from './item'; 5 | export * from './playlist'; 6 | export * from './question_type'; 7 | export * from './question'; 8 | export * from './track'; 9 | -------------------------------------------------------------------------------- /src/js/contracts/item.ts: -------------------------------------------------------------------------------- 1 | import Track from './track'; 2 | 3 | export interface Item { 4 | added_at: string; 5 | added_by: any; 6 | is_local: boolean; 7 | // href: string; 8 | track: Track; 9 | } 10 | 11 | export default Item; 12 | -------------------------------------------------------------------------------- /src/js/contracts/playlist.ts: -------------------------------------------------------------------------------- 1 | import Image from './image'; 2 | import Item from './item'; 3 | 4 | export interface Playlist { 5 | 6 | collaborative: boolean; 7 | description: string; 8 | external_urls: { 9 | spotify: string; 10 | }; 11 | followers: { 12 | href: string; 13 | total: number; 14 | }; 15 | href: string; 16 | id: string; 17 | images: Image[]; 18 | name: string; 19 | owner: { 20 | external_urls: { 21 | spotify: string; 22 | }; 23 | href: string; 24 | id: string; 25 | type: string; 26 | uri: string; 27 | }; 28 | public: boolean; 29 | snapshot_id: string; 30 | tracks: { 31 | href: string; 32 | items: Item[]; 33 | limit: number; 34 | next: string; 35 | offset: number; 36 | previous: string; 37 | total: number; 38 | }; 39 | type: string; 40 | uri: string; 41 | } 42 | 43 | export default Playlist; 44 | -------------------------------------------------------------------------------- /src/js/contracts/question.ts: -------------------------------------------------------------------------------- 1 | import { TemplateRef } from '@angular/core'; 2 | 3 | import { GenericQuestion } from 'app/components'; 4 | import Track from './track'; 5 | import QuestionType from './question_type'; 6 | 7 | export interface Question { 8 | id: number; 9 | type: QuestionType; 10 | track: Track; 11 | title?: string; 12 | description?: string; 13 | answer?: any; 14 | correctAnswer?: any; 15 | answered?: boolean; 16 | wasCorrect?: boolean; 17 | component?: GenericQuestion; 18 | statusTemplate?: TemplateRef; 19 | } 20 | 21 | export default Question; 22 | -------------------------------------------------------------------------------- /src/js/contracts/question_type.ts: -------------------------------------------------------------------------------- 1 | import { QuestionType } from 'app/components/quiz/questions/types'; 2 | export { QuestionType }; 3 | export default QuestionType; 4 | -------------------------------------------------------------------------------- /src/js/contracts/track.ts: -------------------------------------------------------------------------------- 1 | import Album from './album'; 2 | import Artist from './artist'; 3 | 4 | export interface Track { 5 | album: Album; 6 | artists: Artist[]; 7 | available_markets: string[]; 8 | disc_number: number; 9 | duration_ms: number; 10 | explicit: boolean; 11 | href: string; 12 | id: string; 13 | name: string; 14 | popularity: number; 15 | preview_url: string; 16 | track_number: number; 17 | type: string; 18 | uri: string; 19 | } 20 | 21 | export default Track; 22 | -------------------------------------------------------------------------------- /src/js/directives/container-ref.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ViewContainerRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[container-ref]', 5 | }) 6 | export class ContainerRefDirective { 7 | constructor(public viewContainerRef: ViewContainerRef) { } 8 | } 9 | 10 | export default ContainerRefDirective; 11 | -------------------------------------------------------------------------------- /src/js/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './container-ref.directive'; 2 | -------------------------------------------------------------------------------- /src/js/facades/error_handler.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ErrorHandler, isDevMode } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class AppErrorHandler extends ErrorHandler { 5 | 6 | private _modal: any; 7 | 8 | constructor() { 9 | super(); 10 | } 11 | 12 | public handleError(error: any): void { 13 | if (this.isAnimationAbortedError(error)) { 14 | // An animation was interrupted or replaced. 15 | // No need for reporting an error. 16 | return; 17 | } 18 | 19 | if (!isDevMode()) { 20 | this.showErrorMessage(); 21 | return; 22 | } 23 | 24 | super.handleError(error); 25 | } 26 | 27 | protected showErrorMessage(): void { 28 | if (!this._modal) { 29 | this._modal = $('#exception-modal').modal({ 30 | dismissible: false, 31 | opacity: .3, 32 | inDuration: 200, 33 | outDuration: 150 34 | }); 35 | } 36 | 37 | this._modal.modal('open'); 38 | 39 | console.groupCollapsed('Whoops, something went wrong.'); 40 | console.warn('Please run the app in development mode to debug.'); 41 | console.groupEnd(); 42 | } 43 | 44 | private isAnimationAbortedError(error: any): boolean { 45 | return !!error 46 | && typeof error.message === 'string' 47 | && error.message.indexOf('animation_aborted') !== -1; 48 | } 49 | 50 | } 51 | 52 | export default AppErrorHandler; 53 | -------------------------------------------------------------------------------- /src/js/facades/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error_handler'; 2 | -------------------------------------------------------------------------------- /src/js/helpers/common.ts: -------------------------------------------------------------------------------- 1 | export function times(i: number, cb: (n: number) => any, l = i): void { 2 | if (i === 0) { 3 | return; 4 | } 5 | 6 | cb(l - i); 7 | times(i - 1, cb, l); 8 | } 9 | 10 | export function shuffle(array: any[]): any[] { 11 | let currentIndex = array.length; 12 | let temporaryValue: any; 13 | let randomIndex: number; 14 | 15 | while (0 !== currentIndex) { 16 | randomIndex = Math.floor(Math.random() * currentIndex); 17 | currentIndex -= 1; 18 | 19 | temporaryValue = array[currentIndex]; 20 | array[currentIndex] = array[randomIndex]; 21 | array[randomIndex] = temporaryValue; 22 | } 23 | 24 | return array; 25 | } 26 | 27 | export function importToArray(importObject: Record): PropType[] { 28 | const keys = Object.getOwnPropertyNames(importObject); 29 | return keys.filter(key => key.indexOf("__") !== 0).map(key => importObject[key]); 30 | } 31 | -------------------------------------------------------------------------------- /src/js/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './scroll'; 3 | export * from './tracks'; 4 | -------------------------------------------------------------------------------- /src/js/helpers/scroll.ts: -------------------------------------------------------------------------------- 1 | export function scrollTo(element: HTMLElement, to: number, duration: number): Promise { 2 | return new Promise((resolve) => { 3 | _scrollTo(element, to, duration, resolve); 4 | }); 5 | } 6 | 7 | const _scrollTo = (element: HTMLElement, to: number, duration: number, resolve: () => void): void => { 8 | if (duration <= 0) { 9 | return resolve(); 10 | } 11 | 12 | let difference = to - element.scrollTop; 13 | let perTick = difference / duration * 10; 14 | 15 | setTimeout(function() { 16 | element.scrollTop = element.scrollTop + perTick; 17 | 18 | if (element.scrollTop === to) { 19 | return resolve(); 20 | } 21 | 22 | _scrollTo(element, to, duration - 10, resolve); 23 | }, 10); 24 | }; 25 | -------------------------------------------------------------------------------- /src/js/helpers/tracks.ts: -------------------------------------------------------------------------------- 1 | import { Track } from 'app/contracts'; 2 | 3 | export function randomTracksExcluding(exclude: string, amount: number, tracks: Track[]): Track[] { 4 | let randomTracks: Track[] = []; 5 | let taken: number[] = []; 6 | 7 | for (let i = 0; i < amount; i++) { 8 | let current: number; 9 | 10 | do { 11 | current = Math.floor(Math.random() * tracks.length); 12 | } while (taken.indexOf(current) >= 0 || tracks[current].id === exclude); 13 | 14 | taken.push(current); 15 | randomTracks.push(tracks[current]); 16 | } 17 | 18 | return randomTracks; 19 | } 20 | -------------------------------------------------------------------------------- /src/js/main.dev.ts: -------------------------------------------------------------------------------- 1 | console.warn('-----------------------------------'); 2 | console.warn('Application is in Development mode!'); 3 | console.warn('-----------------------------------'); 4 | 5 | import './bootstrap'; 6 | 7 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 8 | import { AppModule } from './modules/app.module'; 9 | 10 | const platform = platformBrowserDynamic(); 11 | 12 | platform.bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /src/js/main.prod.ts: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { AppModule } from './modules/app.module'; 5 | import { enableProdMode } from '@angular/core'; 6 | 7 | enableProdMode(); 8 | 9 | const platform = platformBrowserDynamic(); 10 | 11 | platform.bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/js/modules/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ErrorHandler } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | import { APP_BASE_HREF } from '@angular/common'; 6 | 7 | import { AppRoutes } from 'app/routes'; 8 | import { AppErrorHandler } from 'app/facades'; 9 | 10 | import { AnimatorModule } from 'css-animator'; 11 | import { MaterializeModule } from 'angular2-materialize'; 12 | 13 | import { ContainerRefDirective } from 'app/directives'; 14 | import { QUESTION_COMPONENTS, ANSWER_COMPONENTS } from 'app/components'; 15 | 16 | const appBaseHref = '/* @echo config.appBaseHref */'; 17 | const baseHref = appBaseHref.includes('config.appBaseHref') ? '/' : appBaseHref; 18 | 19 | import { 20 | AppComponent, 21 | LandingComponent, 22 | QuizComponent, 23 | QuestionComponent, 24 | QuizDoneComponent, 25 | QuizNavComponent, 26 | QuizStatusComponent 27 | } from 'app/components'; 28 | 29 | @NgModule({ 30 | imports: [ 31 | BrowserModule, 32 | FormsModule, 33 | HttpModule, 34 | AppRoutes, 35 | MaterializeModule, 36 | AnimatorModule 37 | ], 38 | declarations: [ 39 | AppComponent, 40 | LandingComponent, 41 | QuizComponent, 42 | QuizNavComponent, 43 | QuestionComponent, 44 | QuizDoneComponent, 45 | QuizStatusComponent, 46 | ContainerRefDirective, 47 | ...QUESTION_COMPONENTS, 48 | ...ANSWER_COMPONENTS 49 | ], 50 | entryComponents: QUESTION_COMPONENTS, 51 | providers: [ 52 | { 53 | provide: APP_BASE_HREF, 54 | useValue: baseHref 55 | }, 56 | { provide: ErrorHandler, useClass: AppErrorHandler } 57 | ], 58 | bootstrap: [ 59 | AppComponent 60 | ] 61 | }) 62 | export class AppModule { } 63 | 64 | export default AppModule; 65 | -------------------------------------------------------------------------------- /src/js/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.module'; 2 | -------------------------------------------------------------------------------- /src/js/routes/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LandingComponent, QuizComponent } from 'app/components'; 5 | 6 | const routing: Routes = [ 7 | { path: '', component: LandingComponent }, 8 | { path: 'quiz', component: QuizComponent }, 9 | { path: '**', redirectTo: '' } 10 | ]; 11 | 12 | export const AppRoutes: ModuleWithProviders = RouterModule.forRoot(routing); 13 | 14 | export default AppRoutes; 15 | -------------------------------------------------------------------------------- /src/js/routes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.routes'; 2 | -------------------------------------------------------------------------------- /src/js/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './locale.service'; 2 | export * from './playlist.service'; 3 | export * from './quiz.service'; 4 | -------------------------------------------------------------------------------- /src/js/services/locale.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export interface LocaleData { 4 | name: string; 5 | value: string; 6 | } 7 | 8 | @Injectable() 9 | export class LocaleService { 10 | 11 | private _locale: LocaleData; 12 | 13 | private localStorageAvailable(): boolean { 14 | if (typeof (Storage) !== "undefined" && localStorage) { 15 | return true; 16 | } 17 | 18 | return false; 19 | } 20 | 21 | get locale(): LocaleData { 22 | let region = null; 23 | 24 | if (this.localStorageAvailable()) { 25 | try { 26 | region = JSON.parse(localStorage.getItem('hue.region')); 27 | } catch (e) { } 28 | } 29 | 30 | if (region !== null) { 31 | this._locale = region; 32 | } 33 | 34 | return this._locale || { name: '', value: '' }; 35 | } 36 | 37 | set locale(locale: LocaleData) { 38 | if (this.localStorageAvailable()) { 39 | try { 40 | localStorage.setItem('hue.region', JSON.stringify(locale)); 41 | } catch (e) { } 42 | } 43 | 44 | this._locale = locale; 45 | } 46 | 47 | } 48 | 49 | export default LocaleService; 50 | -------------------------------------------------------------------------------- /src/js/services/playlist.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable, throwError } from 'rxjs'; 2 | import { map, catchError } from 'rxjs/operators'; 3 | 4 | import { Injectable } from '@angular/core'; 5 | import { Http, Response } from '@angular/http'; 6 | 7 | import LocaleService from './locale.service'; 8 | import { Playlist } from 'app/contracts'; 9 | 10 | @Injectable() 11 | export class PlaylistService { 12 | 13 | private _apiUrl = 'api/playlist/random'; 14 | private _mockUrl = 'assets/mock/playlist.json'; 15 | 16 | constructor(private http: Http, private localeService: LocaleService) { 17 | 18 | } 19 | 20 | public getMockPlaylist(): Observable { 21 | let requestUrl = this._mockUrl; 22 | 23 | return this.http.get(requestUrl).pipe( 24 | map(this.extractData), 25 | catchError(this.handleError) 26 | ); 27 | } 28 | 29 | public getPlaylist(): Observable { 30 | let requestUrl = this._apiUrl; 31 | 32 | if (this.localeService.locale && this.localeService.locale.value) { 33 | requestUrl += '?country=' + this.localeService.locale.value; 34 | } 35 | 36 | return this.http.get(requestUrl).pipe( 37 | map(this.extractData), 38 | catchError(this.handleError) 39 | ); 40 | } 41 | 42 | private extractData(res: Response) { 43 | if (res.status < 200 || res.status >= 300) { 44 | throw new Error('Bad response status: ' + res.status); 45 | } 46 | 47 | return res.json() || {}; 48 | } 49 | 50 | private handleError(error: any) { 51 | let errMsg = error.message || 'Error requesting playlist.'; 52 | console.error(errMsg); 53 | return throwError(errMsg); 54 | } 55 | 56 | } 57 | 58 | export default PlaylistService; 59 | -------------------------------------------------------------------------------- /src/js/services/quiz.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { delay, first, map } from 'rxjs/operators'; 3 | 4 | import { Injectable, EventEmitter, isDevMode } from '@angular/core'; 5 | 6 | import { shuffle, scrollTo } from 'app/helpers'; 7 | import { PlaylistService } from './playlist.service'; 8 | 9 | import { Playlist, Track, Question, QuestionType } from 'app/contracts'; 10 | 11 | @Injectable() 12 | export class QuizService { 13 | 14 | private _scrollDuration = 100; 15 | 16 | private _onReady = new EventEmitter(); 17 | private _onActivateQuestion = new EventEmitter(); 18 | private _onCompleted = new EventEmitter(); 19 | private _onClose = new EventEmitter(); 20 | private _onRefresh = new EventEmitter(); 21 | 22 | private _numberOfQuestions: number; 23 | private _progress: number; 24 | private _playlist: Playlist; 25 | private _tracks: Track[]; 26 | private _random: Track[]; 27 | private _questions: Question[]; 28 | 29 | constructor(private playlistService: PlaylistService) { 30 | 31 | } 32 | 33 | public init(numberOfQuestions: number): Observable { 34 | this._numberOfQuestions = numberOfQuestions; 35 | 36 | if (isDevMode()) { 37 | return this.loadMockData(); 38 | } 39 | 40 | return this.loadProductionData(); 41 | } 42 | 43 | public ready(): void { 44 | scrollTo(document.body, 0, this._scrollDuration).then(() => { 45 | this._onReady.emit(); 46 | }); 47 | } 48 | 49 | public close(): void { 50 | scrollTo(document.body, 0, this._scrollDuration).then(() => { 51 | this.onClose.emit(); 52 | }); 53 | } 54 | 55 | public refresh(): void { 56 | this._progress = 0; 57 | this._playlist = null; 58 | this._tracks = []; 59 | this._random = []; 60 | this._questions = []; 61 | 62 | this.init(this._numberOfQuestions) 63 | .pipe(first()) 64 | .subscribe((questions: Question[]) => { 65 | scrollTo(document.body, 0, this._scrollDuration).then(() => { 66 | this.onRefresh.emit(); 67 | }); 68 | }); 69 | } 70 | 71 | public activateQuestion(questionNumber: number): void { 72 | scrollTo(document.body, 0, this._scrollDuration).then(() => { 73 | this._onActivateQuestion.emit(questionNumber); 74 | }); 75 | } 76 | 77 | public completed(): void { 78 | scrollTo(document.body, 0, this._scrollDuration).then(() => { 79 | this._onCompleted.emit(); 80 | }); 81 | } 82 | 83 | public questionById(id: number): Question { 84 | for (let question of this._questions) { 85 | if (question.id === id) { 86 | return question; 87 | } 88 | } 89 | 90 | return null; 91 | } 92 | 93 | public progress(): number { 94 | return this.calculateProgress(); 95 | } 96 | 97 | get onReady(): EventEmitter { 98 | return this._onReady; 99 | } 100 | 101 | get onActivateQuestion(): EventEmitter { 102 | return this._onActivateQuestion; 103 | } 104 | 105 | get onCompleted(): EventEmitter { 106 | return this._onCompleted; 107 | } 108 | 109 | get onClose(): EventEmitter { 110 | return this._onClose; 111 | } 112 | 113 | get onRefresh(): EventEmitter { 114 | return this._onRefresh; 115 | } 116 | 117 | public getCorrectAnswer(question: Question) { 118 | return question.correctAnswer; 119 | } 120 | 121 | private loadProductionData(): Observable { 122 | return this.playlistService.getPlaylist().pipe( 123 | map((playlist: Playlist) => this.extractTracks(playlist)), 124 | map((tracks: Track[]) => this.extractRandom(tracks)), 125 | map((tracks: Track[]) => this.buildQuestions(tracks)) 126 | ); 127 | } 128 | 129 | private loadExistingData(data: Playlist, wait = 1000): Observable { 130 | return of(data).pipe( 131 | delay(wait), 132 | map((playlist: Playlist) => this.extractTracks(playlist)), 133 | map((tracks: Track[]) => this.extractRandom(tracks)), 134 | map((tracks: Track[]) => this.buildQuestions(tracks)) 135 | ); 136 | } 137 | 138 | private loadMockData(wait = 500): Observable { 139 | return this.playlistService.getMockPlaylist().pipe( 140 | delay(wait), 141 | map((playlist: Playlist) => this.extractTracks(playlist)), 142 | map((tracks: Track[]) => this.extractRandom(tracks)), 143 | map((tracks: Track[]) => this.buildQuestions(tracks)), 144 | ); 145 | } 146 | 147 | private calculateProgress(): number { 148 | let count = 0; 149 | 150 | for (let question of this.questions) { 151 | if (question.answered) { 152 | count++; 153 | } 154 | } 155 | 156 | return (count / this.totalQuestions) * 100; 157 | } 158 | 159 | private buildQuestions(randomTracks: Track[]) { 160 | let questions: Question[] = []; 161 | let count = 0; 162 | 163 | for (let track of randomTracks) { 164 | count++; 165 | 166 | let type = count === 1 ? QuestionType.Audio : null; 167 | 168 | type = type === null ? 169 | Math.floor(Math.random() * (Object.keys(QuestionType).length / 2)) 170 | : type; 171 | 172 | let question: Question = { 173 | id: -1, 174 | type: type, 175 | track: track, 176 | answer: undefined, 177 | correctAnswer: undefined, 178 | answered: false, 179 | wasCorrect: false 180 | }; 181 | 182 | questions.push(question); 183 | } 184 | 185 | count = 1; 186 | this._questions = shuffle(questions.slice(1)).map((question) => { 187 | question.id = ++count; 188 | return question; 189 | }); 190 | 191 | questions[0].id = 1; 192 | this._questions.unshift(questions[0]); 193 | 194 | return this._questions; 195 | } 196 | 197 | private extractTracks(playlist: Playlist) { 198 | let tracks: Track[] = []; 199 | 200 | playlist.tracks.items.forEach((item, key) => { 201 | tracks.push(item.track); 202 | }); 203 | 204 | this._playlist = playlist; 205 | this._tracks = tracks; 206 | 207 | return tracks; 208 | } 209 | 210 | private extractRandom(tracks: Track[]) { 211 | return this._random = this.getRandomTracks(this._numberOfQuestions); 212 | } 213 | 214 | private getRandomTracks(amount: number): Track[] { 215 | let randomTracks: Track[] = []; 216 | let taken: number[] = []; 217 | 218 | for (let i = 0; i < amount; i++) { 219 | let current: number; 220 | let track: Track; 221 | 222 | do { 223 | current = Math.floor(Math.random() * this._tracks.length); 224 | track = this._tracks[current]; 225 | } while ( 226 | taken.indexOf(current) >= 0 || 227 | !track.preview_url || 228 | !track.album || 229 | !track.album.images || 230 | !track.album.images[0] || 231 | !track.album.images[0].url 232 | ); 233 | 234 | taken.push(current); 235 | randomTracks.push(track); 236 | } 237 | 238 | return randomTracks; 239 | } 240 | 241 | get totalQuestions(): number { 242 | return this.questions.length; 243 | } 244 | 245 | get questions(): Question[] { 246 | return this._questions || []; 247 | } 248 | 249 | get playlist(): Playlist { 250 | return this._playlist; 251 | } 252 | 253 | get tracks(): Track[] { 254 | return this._tracks; 255 | } 256 | 257 | } 258 | 259 | export default QuizService; 260 | -------------------------------------------------------------------------------- /src/types/import.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html' { 2 | var _: string; 3 | export default _; 4 | } 5 | 6 | declare module '*.css' { 7 | var _: string; 8 | export default _; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/iscroll.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'iscroll' { 2 | export = IScroll; 3 | } 4 | 5 | interface IScrollOptions { 6 | deceleration?: number; 7 | } 8 | -------------------------------------------------------------------------------- /system.config.js: -------------------------------------------------------------------------------- 1 | SystemJS.config({ 2 | paths: { 3 | "github:": "jspm_packages/github/", 4 | "npm:": "jspm_packages/npm/", 5 | "app/": "src/js/" 6 | }, 7 | browserConfig: { 8 | "baseURL": "/" 9 | }, 10 | devConfig: { 11 | "map": { 12 | "ts-runtime": "npm:babel-runtime@6.26.0", 13 | "core-js": "npm:core-js@3.1.3", 14 | "plugin-babel": "npm:systemjs-plugin-babel@0.0.25", 15 | "text": "github:systemjs/plugin-text@0.0.11", 16 | "ts": "github:frankwallis/plugin-typescript@9.0.0", 17 | "typescript": "npm:typescript@3.5.1", 18 | "source-map-support": "npm:source-map-support@0.5.12" 19 | }, 20 | "packages": { 21 | "npm:babel-runtime@6.26.0": { 22 | "map": { 23 | "core-js": "npm:core-js@2.6.9", 24 | "regenerator-runtime": "npm:regenerator-runtime@0.11.1" 25 | } 26 | }, 27 | "npm:source-map-support@0.5.12": { 28 | "map": { 29 | "buffer-from": "npm:buffer-from@1.1.1", 30 | "source-map": "npm:source-map@0.6.1" 31 | } 32 | } 33 | } 34 | }, 35 | transpiler: "plugin-babel", 36 | typescriptOptions: { 37 | "tsconfig": "tsconfig.app.json" 38 | }, 39 | packages: { 40 | "app": { 41 | "main": "main.prod", 42 | "defaultExtension": "ts", 43 | "map": { 44 | "./components": "./components/index", 45 | "./components/app": "./components/app/index", 46 | "./components/landing": "./components/landing/index", 47 | "./components/quiz": "./components/quiz/index", 48 | "./components/quiz/answers": "./components/quiz/answers/index", 49 | "./components/quiz/done": "./components/quiz/done/index", 50 | "./components/quiz/nav": "./components/quiz/nav/index", 51 | "./components/quiz/questions": "./components/quiz/questions/index", 52 | "./components/quiz/status": "./components/quiz/status/index", 53 | "./contracts": "./contracts/index", 54 | "./directives": "./directives/index", 55 | "./facades": "./facades/index", 56 | "./helpers": "./helpers/index", 57 | "./modules": "./modules/index", 58 | "./routes": "./routes/index", 59 | "./services": "./services/index" 60 | }, 61 | "meta": { 62 | "*.ts": { 63 | "loader": "ts" 64 | }, 65 | "*.js": { 66 | "loader": "plugin-babel" 67 | }, 68 | "*.css": { 69 | "loader": "text" 70 | }, 71 | "*.html": { 72 | "loader": "text" 73 | } 74 | } 75 | } 76 | }, 77 | map: { 78 | "babel-runtime": "npm:babel-runtime@6.26.0" 79 | } 80 | }); 81 | 82 | SystemJS.config({ 83 | packageConfigPaths: [ 84 | "npm:@*/*.json", 85 | "npm:*.json", 86 | "github:*/*.json" 87 | ], 88 | map: { 89 | "@angular/common": "npm:@angular/common@8.0.0", 90 | "@angular/compiler": "npm:@angular/compiler@8.0.0", 91 | "@angular/core": "npm:@angular/core@8.0.0", 92 | "@angular/forms": "npm:@angular/forms@8.0.0", 93 | "@angular/http": "npm:@angular/http@7.2.15", 94 | "@angular/platform-browser": "npm:@angular/platform-browser@8.0.0", 95 | "@angular/platform-browser-dynamic": "npm:@angular/platform-browser-dynamic@8.0.0", 96 | "@angular/router": "npm:@angular/router@8.0.0", 97 | "angular2-materialize": "npm:angular2-materialize@15.1.10", 98 | "assert": "npm:jspm-nodelibs-assert@0.2.1", 99 | "buffer": "npm:jspm-nodelibs-buffer@0.2.3", 100 | "child_process": "npm:jspm-nodelibs-child_process@0.2.1", 101 | "constants": "npm:jspm-nodelibs-constants@0.2.1", 102 | "crypto": "npm:jspm-nodelibs-crypto@0.2.1", 103 | "css-animator": "npm:css-animator@2.3.1", 104 | "events": "npm:jspm-nodelibs-events@0.2.2", 105 | "fs": "npm:jspm-nodelibs-fs@0.2.1", 106 | "hammerjs": "npm:hammerjs@2.0.8", 107 | "http": "npm:jspm-nodelibs-http@0.2.0", 108 | "iscroll": "npm:iscroll@5.2.0", 109 | "jquery": "npm:jquery@2.2.4", 110 | "materialize-css": "npm:materialize-css@0.100.2", 111 | "module": "npm:jspm-nodelibs-module@0.2.1", 112 | "net": "npm:jspm-nodelibs-net@0.2.1", 113 | "os": "npm:jspm-nodelibs-os@0.2.2", 114 | "path": "npm:jspm-nodelibs-path@0.2.3", 115 | "process": "npm:jspm-nodelibs-process@0.2.1", 116 | "readline": "npm:jspm-nodelibs-readline@0.2.1", 117 | "reflect-metadata": "npm:reflect-metadata@0.1.13", 118 | "rxjs": "npm:rxjs@6.5.2", 119 | "stream": "npm:jspm-nodelibs-stream@0.2.1", 120 | "string_decoder": "npm:jspm-nodelibs-string_decoder@0.2.2", 121 | "timers": "npm:jspm-nodelibs-timers@0.2.1", 122 | "tty": "npm:jspm-nodelibs-tty@0.2.1", 123 | "url": "npm:jspm-nodelibs-url@0.2.1", 124 | "util": "npm:jspm-nodelibs-util@0.2.2", 125 | "vm": "npm:jspm-nodelibs-vm@0.2.1", 126 | "zlib": "npm:jspm-nodelibs-zlib@0.2.3", 127 | "zone.js": "npm:zone.js@0.9.1" 128 | }, 129 | packages: { 130 | "npm:browserify-rsa@4.0.1": { 131 | "map": { 132 | "bn.js": "npm:bn.js@4.11.8", 133 | "randombytes": "npm:randombytes@2.1.0" 134 | } 135 | }, 136 | "npm:buffer-xor@1.0.3": { 137 | "map": {} 138 | }, 139 | "npm:core-util-is@1.0.2": { 140 | "map": {} 141 | }, 142 | "npm:des.js@1.0.0": { 143 | "map": { 144 | "inherits": "npm:inherits@2.0.3", 145 | "minimalistic-assert": "npm:minimalistic-assert@1.0.1" 146 | } 147 | }, 148 | "npm:string_decoder@0.10.31": { 149 | "map": {} 150 | }, 151 | "npm:url@0.11.0": { 152 | "map": { 153 | "punycode": "npm:punycode@1.3.2", 154 | "querystring": "npm:querystring@0.2.0" 155 | } 156 | }, 157 | "npm:timers-browserify@1.4.2": { 158 | "map": { 159 | "process": "npm:process@0.11.10" 160 | } 161 | }, 162 | "npm:jspm-nodelibs-http@0.2.0": { 163 | "map": { 164 | "http-browserify": "npm:stream-http@2.8.3" 165 | } 166 | }, 167 | "npm:browserify-sign@4.0.4": { 168 | "map": { 169 | "create-hmac": "npm:create-hmac@1.1.7", 170 | "parse-asn1": "npm:parse-asn1@5.1.4", 171 | "elliptic": "npm:elliptic@6.4.1", 172 | "bn.js": "npm:bn.js@4.11.8", 173 | "create-hash": "npm:create-hash@1.2.0", 174 | "browserify-rsa": "npm:browserify-rsa@4.0.1", 175 | "inherits": "npm:inherits@2.0.3" 176 | } 177 | }, 178 | "npm:hmac-drbg@1.0.1": { 179 | "map": { 180 | "hash.js": "npm:hash.js@1.1.7", 181 | "minimalistic-assert": "npm:minimalistic-assert@1.0.1", 182 | "minimalistic-crypto-utils": "npm:minimalistic-crypto-utils@1.0.1" 183 | } 184 | }, 185 | "npm:jspm-nodelibs-zlib@0.2.3": { 186 | "map": { 187 | "browserify-zlib": "npm:browserify-zlib@0.1.4" 188 | } 189 | }, 190 | "npm:browserify-zlib@0.1.4": { 191 | "map": { 192 | "readable-stream": "npm:readable-stream@2.3.6", 193 | "pako": "npm:pako@0.2.9" 194 | } 195 | }, 196 | "npm:jspm-nodelibs-timers@0.2.1": { 197 | "map": { 198 | "timers-browserify": "npm:timers-browserify@1.4.2" 199 | } 200 | }, 201 | "npm:jspm-nodelibs-crypto@0.2.1": { 202 | "map": { 203 | "crypto-browserify": "npm:crypto-browserify@3.12.0" 204 | } 205 | }, 206 | "npm:jspm-nodelibs-buffer@0.2.3": { 207 | "map": { 208 | "buffer": "npm:buffer@5.2.1" 209 | } 210 | }, 211 | "npm:jspm-nodelibs-stream@0.2.1": { 212 | "map": { 213 | "stream-browserify": "npm:stream-browserify@2.0.2" 214 | } 215 | }, 216 | "npm:jspm-nodelibs-url@0.2.1": { 217 | "map": { 218 | "url": "npm:url@0.11.0" 219 | } 220 | }, 221 | "npm:jspm-nodelibs-os@0.2.2": { 222 | "map": { 223 | "os-browserify": "npm:os-browserify@0.3.0" 224 | } 225 | }, 226 | "npm:cipher-base@1.0.4": { 227 | "map": { 228 | "safe-buffer": "npm:safe-buffer@5.1.2", 229 | "inherits": "npm:inherits@2.0.3" 230 | } 231 | }, 232 | "npm:evp_bytestokey@1.0.3": { 233 | "map": { 234 | "safe-buffer": "npm:safe-buffer@5.1.2", 235 | "md5.js": "npm:md5.js@1.3.5" 236 | } 237 | }, 238 | "npm:hash-base@3.0.4": { 239 | "map": { 240 | "safe-buffer": "npm:safe-buffer@5.1.2", 241 | "inherits": "npm:inherits@2.0.3" 242 | } 243 | }, 244 | "npm:materialize-css@0.100.2": { 245 | "map": { 246 | "hammerjs": "npm:hammerjs@2.0.8", 247 | "jquery": "npm:jquery@2.2.4" 248 | } 249 | }, 250 | "npm:crypto-browserify@3.12.0": { 251 | "map": { 252 | "randomfill": "npm:randomfill@1.0.4", 253 | "public-encrypt": "npm:public-encrypt@4.0.3", 254 | "inherits": "npm:inherits@2.0.3", 255 | "browserify-sign": "npm:browserify-sign@4.0.4", 256 | "create-hmac": "npm:create-hmac@1.1.7", 257 | "browserify-cipher": "npm:browserify-cipher@1.0.1", 258 | "diffie-hellman": "npm:diffie-hellman@5.0.3", 259 | "create-hash": "npm:create-hash@1.2.0", 260 | "create-ecdh": "npm:create-ecdh@4.0.3", 261 | "pbkdf2": "npm:pbkdf2@3.0.17", 262 | "randombytes": "npm:randombytes@2.1.0" 263 | } 264 | }, 265 | "npm:miller-rabin@4.0.1": { 266 | "map": { 267 | "bn.js": "npm:bn.js@4.11.8", 268 | "brorand": "npm:brorand@1.1.0" 269 | } 270 | }, 271 | "npm:jspm-nodelibs-string_decoder@0.2.2": { 272 | "map": { 273 | "string_decoder": "npm:string_decoder@0.10.31" 274 | } 275 | }, 276 | "npm:readable-stream@2.3.6": { 277 | "map": { 278 | "string_decoder": "npm:string_decoder@1.1.1", 279 | "inherits": "npm:inherits@2.0.3", 280 | "core-util-is": "npm:core-util-is@1.0.2", 281 | "process-nextick-args": "npm:process-nextick-args@2.0.0", 282 | "util-deprecate": "npm:util-deprecate@1.0.2", 283 | "isarray": "npm:isarray@1.0.0", 284 | "safe-buffer": "npm:safe-buffer@5.1.2" 285 | } 286 | }, 287 | "npm:randomfill@1.0.4": { 288 | "map": { 289 | "randombytes": "npm:randombytes@2.1.0", 290 | "safe-buffer": "npm:safe-buffer@5.1.2" 291 | } 292 | }, 293 | "npm:string_decoder@1.1.1": { 294 | "map": { 295 | "safe-buffer": "npm:safe-buffer@5.1.2" 296 | } 297 | }, 298 | "npm:browserify-aes@1.2.0": { 299 | "map": { 300 | "cipher-base": "npm:cipher-base@1.0.4", 301 | "create-hash": "npm:create-hash@1.2.0", 302 | "evp_bytestokey": "npm:evp_bytestokey@1.0.3", 303 | "inherits": "npm:inherits@2.0.3", 304 | "safe-buffer": "npm:safe-buffer@5.1.2", 305 | "buffer-xor": "npm:buffer-xor@1.0.3" 306 | } 307 | }, 308 | "npm:sha.js@2.4.11": { 309 | "map": { 310 | "inherits": "npm:inherits@2.0.3", 311 | "safe-buffer": "npm:safe-buffer@5.1.2" 312 | } 313 | }, 314 | "npm:asn1.js@4.10.1": { 315 | "map": { 316 | "bn.js": "npm:bn.js@4.11.8", 317 | "minimalistic-assert": "npm:minimalistic-assert@1.0.1", 318 | "inherits": "npm:inherits@2.0.3" 319 | } 320 | }, 321 | "npm:stream-http@2.8.3": { 322 | "map": { 323 | "inherits": "npm:inherits@2.0.3", 324 | "builtin-status-codes": "npm:builtin-status-codes@3.0.0", 325 | "xtend": "npm:xtend@4.0.1", 326 | "to-arraybuffer": "npm:to-arraybuffer@1.0.1", 327 | "readable-stream": "npm:readable-stream@2.3.6" 328 | } 329 | }, 330 | "npm:buffer@5.2.1": { 331 | "map": { 332 | "ieee754": "npm:ieee754@1.1.13", 333 | "base64-js": "npm:base64-js@1.3.0" 334 | } 335 | }, 336 | "npm:create-hash@1.2.0": { 337 | "map": { 338 | "inherits": "npm:inherits@2.0.3", 339 | "md5.js": "npm:md5.js@1.3.5", 340 | "cipher-base": "npm:cipher-base@1.0.4", 341 | "ripemd160": "npm:ripemd160@2.0.2", 342 | "sha.js": "npm:sha.js@2.4.11" 343 | } 344 | }, 345 | "npm:create-hmac@1.1.7": { 346 | "map": { 347 | "create-hash": "npm:create-hash@1.2.0", 348 | "inherits": "npm:inherits@2.0.3", 349 | "cipher-base": "npm:cipher-base@1.0.4", 350 | "ripemd160": "npm:ripemd160@2.0.2", 351 | "sha.js": "npm:sha.js@2.4.11", 352 | "safe-buffer": "npm:safe-buffer@5.1.2" 353 | } 354 | }, 355 | "npm:browserify-cipher@1.0.1": { 356 | "map": { 357 | "evp_bytestokey": "npm:evp_bytestokey@1.0.3", 358 | "browserify-des": "npm:browserify-des@1.0.2", 359 | "browserify-aes": "npm:browserify-aes@1.2.0" 360 | } 361 | }, 362 | "npm:diffie-hellman@5.0.3": { 363 | "map": { 364 | "randombytes": "npm:randombytes@2.1.0", 365 | "bn.js": "npm:bn.js@4.11.8", 366 | "miller-rabin": "npm:miller-rabin@4.0.1" 367 | } 368 | }, 369 | "npm:create-ecdh@4.0.3": { 370 | "map": { 371 | "bn.js": "npm:bn.js@4.11.8", 372 | "elliptic": "npm:elliptic@6.4.1" 373 | } 374 | }, 375 | "npm:browserify-des@1.0.2": { 376 | "map": { 377 | "cipher-base": "npm:cipher-base@1.0.4", 378 | "inherits": "npm:inherits@2.0.3", 379 | "des.js": "npm:des.js@1.0.0", 380 | "safe-buffer": "npm:safe-buffer@5.1.2" 381 | } 382 | }, 383 | "npm:ripemd160@2.0.2": { 384 | "map": { 385 | "inherits": "npm:inherits@2.0.3", 386 | "hash-base": "npm:hash-base@3.0.4" 387 | } 388 | }, 389 | "npm:elliptic@6.4.1": { 390 | "map": { 391 | "bn.js": "npm:bn.js@4.11.8", 392 | "inherits": "npm:inherits@2.0.3", 393 | "brorand": "npm:brorand@1.1.0", 394 | "hmac-drbg": "npm:hmac-drbg@1.0.1", 395 | "minimalistic-crypto-utils": "npm:minimalistic-crypto-utils@1.0.1", 396 | "hash.js": "npm:hash.js@1.1.7", 397 | "minimalistic-assert": "npm:minimalistic-assert@1.0.1" 398 | } 399 | }, 400 | "npm:rxjs@6.5.2": { 401 | "map": { 402 | "tslib": "npm:tslib@1.10.0" 403 | } 404 | }, 405 | "npm:stream-browserify@2.0.2": { 406 | "map": { 407 | "inherits": "npm:inherits@2.0.3", 408 | "readable-stream": "npm:readable-stream@2.3.6" 409 | } 410 | }, 411 | "npm:pbkdf2@3.0.17": { 412 | "map": { 413 | "create-hash": "npm:create-hash@1.2.0", 414 | "create-hmac": "npm:create-hmac@1.1.7", 415 | "ripemd160": "npm:ripemd160@2.0.2", 416 | "safe-buffer": "npm:safe-buffer@5.1.2", 417 | "sha.js": "npm:sha.js@2.4.11" 418 | } 419 | }, 420 | "npm:public-encrypt@4.0.3": { 421 | "map": { 422 | "create-hash": "npm:create-hash@1.2.0", 423 | "randombytes": "npm:randombytes@2.1.0", 424 | "safe-buffer": "npm:safe-buffer@5.1.2", 425 | "browserify-rsa": "npm:browserify-rsa@4.0.1", 426 | "parse-asn1": "npm:parse-asn1@5.1.4", 427 | "bn.js": "npm:bn.js@4.11.8" 428 | } 429 | }, 430 | "npm:randombytes@2.1.0": { 431 | "map": { 432 | "safe-buffer": "npm:safe-buffer@5.1.2" 433 | } 434 | }, 435 | "npm:md5.js@1.3.5": { 436 | "map": { 437 | "safe-buffer": "npm:safe-buffer@5.1.2", 438 | "inherits": "npm:inherits@2.0.3", 439 | "hash-base": "npm:hash-base@3.0.4" 440 | } 441 | }, 442 | "npm:parse-asn1@5.1.4": { 443 | "map": { 444 | "browserify-aes": "npm:browserify-aes@1.2.0", 445 | "create-hash": "npm:create-hash@1.2.0", 446 | "pbkdf2": "npm:pbkdf2@3.0.17", 447 | "evp_bytestokey": "npm:evp_bytestokey@1.0.3", 448 | "safe-buffer": "npm:safe-buffer@5.1.2", 449 | "asn1.js": "npm:asn1.js@4.10.1" 450 | } 451 | }, 452 | "npm:hash.js@1.1.7": { 453 | "map": { 454 | "inherits": "npm:inherits@2.0.3", 455 | "minimalistic-assert": "npm:minimalistic-assert@1.0.1" 456 | } 457 | }, 458 | "npm:@angular/common@8.0.0": { 459 | "map": { 460 | "tslib": "npm:tslib@1.10.0" 461 | } 462 | }, 463 | "npm:@angular/core@8.0.0": { 464 | "map": { 465 | "tslib": "npm:tslib@1.10.0" 466 | } 467 | }, 468 | "npm:@angular/compiler@8.0.0": { 469 | "map": { 470 | "tslib": "npm:tslib@1.10.0" 471 | } 472 | }, 473 | "npm:@angular/platform-browser@8.0.0": { 474 | "map": { 475 | "tslib": "npm:tslib@1.10.0" 476 | } 477 | }, 478 | "npm:@angular/router@8.0.0": { 479 | "map": { 480 | "tslib": "npm:tslib@1.10.0" 481 | } 482 | }, 483 | "npm:@angular/platform-browser-dynamic@8.0.0": { 484 | "map": { 485 | "tslib": "npm:tslib@1.10.0" 486 | } 487 | }, 488 | "npm:@angular/http@7.2.15": { 489 | "map": { 490 | "tslib": "npm:tslib@1.10.0" 491 | } 492 | }, 493 | "npm:@angular/forms@8.0.0": { 494 | "map": { 495 | "tslib": "npm:tslib@1.10.0" 496 | } 497 | } 498 | } 499 | }); 500 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "allowJs": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "moduleResolution": "node", 9 | "module": "system", 10 | "noImplicitAny": true, 11 | "removeComments": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "sourceMap": false, 14 | "target": "es5", 15 | "lib": [ 16 | "es2015", 17 | "dom" 18 | ], 19 | "typeRoots": [ 20 | "node_modules/@types", 21 | "src/types" 22 | ], 23 | "paths": { 24 | "app/*": [ 25 | "src/js/*", 26 | "src/js/*/index" 27 | ] 28 | } 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "jspm_packages", 33 | "typings", 34 | "server", 35 | "dist" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "app/*": [ 7 | "src/js/*", 8 | "src/js/*/index" 9 | ], 10 | "@angular/common": [ 11 | "jspm_packages/npm/@angular/common@8.0.0" 12 | ], 13 | "@angular/common/*": [ 14 | "jspm_packages/npm/@angular/common@8.0.0/*", 15 | "jspm_packages/npm/@angular/common@8.0.0/*/index" 16 | ], 17 | "@angular/compiler": [ 18 | "jspm_packages/npm/@angular/compiler@8.0.0" 19 | ], 20 | "@angular/compiler/*": [ 21 | "jspm_packages/npm/@angular/compiler@8.0.0/*", 22 | "jspm_packages/npm/@angular/compiler@8.0.0/*/index" 23 | ], 24 | "@angular/core": [ 25 | "jspm_packages/npm/@angular/core@8.0.0" 26 | ], 27 | "@angular/core/*": [ 28 | "jspm_packages/npm/@angular/core@8.0.0/*", 29 | "jspm_packages/npm/@angular/core@8.0.0/*/index" 30 | ], 31 | "@angular/forms": [ 32 | "jspm_packages/npm/@angular/forms@8.0.0" 33 | ], 34 | "@angular/forms/*": [ 35 | "jspm_packages/npm/@angular/forms@8.0.0/*", 36 | "jspm_packages/npm/@angular/forms@8.0.0/*/index" 37 | ], 38 | "@angular/http": [ 39 | "jspm_packages/npm/@angular/http@7.2.15" 40 | ], 41 | "@angular/http/*": [ 42 | "jspm_packages/npm/@angular/http@7.2.15/*", 43 | "jspm_packages/npm/@angular/http@7.2.15/*/index" 44 | ], 45 | "@angular/platform-browser": [ 46 | "jspm_packages/npm/@angular/platform-browser@8.0.0" 47 | ], 48 | "@angular/platform-browser/*": [ 49 | "jspm_packages/npm/@angular/platform-browser@8.0.0/*", 50 | "jspm_packages/npm/@angular/platform-browser@8.0.0/*/index" 51 | ], 52 | "@angular/platform-browser-dynamic": [ 53 | "jspm_packages/npm/@angular/platform-browser-dynamic@8.0.0" 54 | ], 55 | "@angular/platform-browser-dynamic/*": [ 56 | "jspm_packages/npm/@angular/platform-browser-dynamic@8.0.0/*", 57 | "jspm_packages/npm/@angular/platform-browser-dynamic@8.0.0/*/index" 58 | ], 59 | "@angular/router": [ 60 | "jspm_packages/npm/@angular/router@8.0.0" 61 | ], 62 | "@angular/router/*": [ 63 | "jspm_packages/npm/@angular/router@8.0.0/*", 64 | "jspm_packages/npm/@angular/router@8.0.0/*/index" 65 | ], 66 | "angular2-materialize": [ 67 | "jspm_packages/npm/angular2-materialize@15.1.10" 68 | ], 69 | "angular2-materialize/*": [ 70 | "jspm_packages/npm/angular2-materialize@15.1.10/*", 71 | "jspm_packages/npm/angular2-materialize@15.1.10/*/index" 72 | ], 73 | "assert": [ 74 | "jspm_packages/npm/jspm-nodelibs-assert@0.2.1" 75 | ], 76 | "assert/*": [ 77 | "jspm_packages/npm/jspm-nodelibs-assert@0.2.1/*", 78 | "jspm_packages/npm/jspm-nodelibs-assert@0.2.1/*/index" 79 | ], 80 | "buffer": [ 81 | "jspm_packages/npm/jspm-nodelibs-buffer@0.2.3" 82 | ], 83 | "buffer/*": [ 84 | "jspm_packages/npm/jspm-nodelibs-buffer@0.2.3/*", 85 | "jspm_packages/npm/jspm-nodelibs-buffer@0.2.3/*/index" 86 | ], 87 | "child_process": [ 88 | "jspm_packages/npm/jspm-nodelibs-child_process@0.2.1" 89 | ], 90 | "child_process/*": [ 91 | "jspm_packages/npm/jspm-nodelibs-child_process@0.2.1/*", 92 | "jspm_packages/npm/jspm-nodelibs-child_process@0.2.1/*/index" 93 | ], 94 | "constants": [ 95 | "jspm_packages/npm/jspm-nodelibs-constants@0.2.1" 96 | ], 97 | "constants/*": [ 98 | "jspm_packages/npm/jspm-nodelibs-constants@0.2.1/*", 99 | "jspm_packages/npm/jspm-nodelibs-constants@0.2.1/*/index" 100 | ], 101 | "crypto": [ 102 | "jspm_packages/npm/jspm-nodelibs-crypto@0.2.1" 103 | ], 104 | "crypto/*": [ 105 | "jspm_packages/npm/jspm-nodelibs-crypto@0.2.1/*", 106 | "jspm_packages/npm/jspm-nodelibs-crypto@0.2.1/*/index" 107 | ], 108 | "css-animator": [ 109 | "jspm_packages/npm/css-animator@2.3.1" 110 | ], 111 | "css-animator/*": [ 112 | "jspm_packages/npm/css-animator@2.3.1/*", 113 | "jspm_packages/npm/css-animator@2.3.1/*/index" 114 | ], 115 | "events": [ 116 | "jspm_packages/npm/jspm-nodelibs-events@0.2.2" 117 | ], 118 | "events/*": [ 119 | "jspm_packages/npm/jspm-nodelibs-events@0.2.2/*", 120 | "jspm_packages/npm/jspm-nodelibs-events@0.2.2/*/index" 121 | ], 122 | "fs": [ 123 | "jspm_packages/npm/jspm-nodelibs-fs@0.2.1" 124 | ], 125 | "fs/*": [ 126 | "jspm_packages/npm/jspm-nodelibs-fs@0.2.1/*", 127 | "jspm_packages/npm/jspm-nodelibs-fs@0.2.1/*/index" 128 | ], 129 | "hammerjs": [ 130 | "jspm_packages/npm/hammerjs@2.0.8" 131 | ], 132 | "hammerjs/*": [ 133 | "jspm_packages/npm/hammerjs@2.0.8/*", 134 | "jspm_packages/npm/hammerjs@2.0.8/*/index" 135 | ], 136 | "http": [ 137 | "jspm_packages/npm/jspm-nodelibs-http@0.2.0" 138 | ], 139 | "http/*": [ 140 | "jspm_packages/npm/jspm-nodelibs-http@0.2.0/*", 141 | "jspm_packages/npm/jspm-nodelibs-http@0.2.0/*/index" 142 | ], 143 | "iscroll": [ 144 | "jspm_packages/npm/iscroll@5.2.0" 145 | ], 146 | "iscroll/*": [ 147 | "jspm_packages/npm/iscroll@5.2.0/*", 148 | "jspm_packages/npm/iscroll@5.2.0/*/index" 149 | ], 150 | "jquery": [ 151 | "jspm_packages/npm/jquery@2.2.4" 152 | ], 153 | "jquery/*": [ 154 | "jspm_packages/npm/jquery@2.2.4/*", 155 | "jspm_packages/npm/jquery@2.2.4/*/index" 156 | ], 157 | "materialize-css": [ 158 | "jspm_packages/npm/materialize-css@0.100.2" 159 | ], 160 | "materialize-css/*": [ 161 | "jspm_packages/npm/materialize-css@0.100.2/*", 162 | "jspm_packages/npm/materialize-css@0.100.2/*/index" 163 | ], 164 | "module": [ 165 | "jspm_packages/npm/jspm-nodelibs-module@0.2.1" 166 | ], 167 | "module/*": [ 168 | "jspm_packages/npm/jspm-nodelibs-module@0.2.1/*", 169 | "jspm_packages/npm/jspm-nodelibs-module@0.2.1/*/index" 170 | ], 171 | "net": [ 172 | "jspm_packages/npm/jspm-nodelibs-net@0.2.1" 173 | ], 174 | "net/*": [ 175 | "jspm_packages/npm/jspm-nodelibs-net@0.2.1/*", 176 | "jspm_packages/npm/jspm-nodelibs-net@0.2.1/*/index" 177 | ], 178 | "os": [ 179 | "jspm_packages/npm/jspm-nodelibs-os@0.2.2" 180 | ], 181 | "os/*": [ 182 | "jspm_packages/npm/jspm-nodelibs-os@0.2.2/*", 183 | "jspm_packages/npm/jspm-nodelibs-os@0.2.2/*/index" 184 | ], 185 | "path": [ 186 | "jspm_packages/npm/jspm-nodelibs-path@0.2.3" 187 | ], 188 | "path/*": [ 189 | "jspm_packages/npm/jspm-nodelibs-path@0.2.3/*", 190 | "jspm_packages/npm/jspm-nodelibs-path@0.2.3/*/index" 191 | ], 192 | "process": [ 193 | "jspm_packages/npm/jspm-nodelibs-process@0.2.1" 194 | ], 195 | "process/*": [ 196 | "jspm_packages/npm/jspm-nodelibs-process@0.2.1/*", 197 | "jspm_packages/npm/jspm-nodelibs-process@0.2.1/*/index" 198 | ], 199 | "reflect-metadata": [ 200 | "jspm_packages/npm/reflect-metadata@0.1.13" 201 | ], 202 | "reflect-metadata/*": [ 203 | "jspm_packages/npm/reflect-metadata@0.1.13/*", 204 | "jspm_packages/npm/reflect-metadata@0.1.13/*/index" 205 | ], 206 | "rxjs": [ 207 | "jspm_packages/npm/rxjs@6.5.2" 208 | ], 209 | "rxjs/*": [ 210 | "jspm_packages/npm/rxjs@6.5.2/*", 211 | "jspm_packages/npm/rxjs@6.5.2/*/index" 212 | ], 213 | "stream": [ 214 | "jspm_packages/npm/jspm-nodelibs-stream@0.2.1" 215 | ], 216 | "stream/*": [ 217 | "jspm_packages/npm/jspm-nodelibs-stream@0.2.1/*", 218 | "jspm_packages/npm/jspm-nodelibs-stream@0.2.1/*/index" 219 | ], 220 | "string_decoder": [ 221 | "jspm_packages/npm/jspm-nodelibs-string_decoder@0.2.2" 222 | ], 223 | "string_decoder/*": [ 224 | "jspm_packages/npm/jspm-nodelibs-string_decoder@0.2.2/*", 225 | "jspm_packages/npm/jspm-nodelibs-string_decoder@0.2.2/*/index" 226 | ], 227 | "timers": [ 228 | "jspm_packages/npm/jspm-nodelibs-timers@0.2.1" 229 | ], 230 | "timers/*": [ 231 | "jspm_packages/npm/jspm-nodelibs-timers@0.2.1/*", 232 | "jspm_packages/npm/jspm-nodelibs-timers@0.2.1/*/index" 233 | ], 234 | "tty": [ 235 | "jspm_packages/npm/jspm-nodelibs-tty@0.2.1" 236 | ], 237 | "tty/*": [ 238 | "jspm_packages/npm/jspm-nodelibs-tty@0.2.1/*", 239 | "jspm_packages/npm/jspm-nodelibs-tty@0.2.1/*/index" 240 | ], 241 | "typescript": [ 242 | "jspm_packages/npm/typescript@3.5.1" 243 | ], 244 | "typescript/*": [ 245 | "jspm_packages/npm/typescript@3.5.1/*", 246 | "jspm_packages/npm/typescript@3.5.1/*/index" 247 | ], 248 | "url": [ 249 | "jspm_packages/npm/jspm-nodelibs-url@0.2.1" 250 | ], 251 | "url/*": [ 252 | "jspm_packages/npm/jspm-nodelibs-url@0.2.1/*", 253 | "jspm_packages/npm/jspm-nodelibs-url@0.2.1/*/index" 254 | ], 255 | "util": [ 256 | "jspm_packages/npm/jspm-nodelibs-util@0.2.2" 257 | ], 258 | "util/*": [ 259 | "jspm_packages/npm/jspm-nodelibs-util@0.2.2/*", 260 | "jspm_packages/npm/jspm-nodelibs-util@0.2.2/*/index" 261 | ], 262 | "vm": [ 263 | "jspm_packages/npm/jspm-nodelibs-vm@0.2.1" 264 | ], 265 | "vm/*": [ 266 | "jspm_packages/npm/jspm-nodelibs-vm@0.2.1/*", 267 | "jspm_packages/npm/jspm-nodelibs-vm@0.2.1/*/index" 268 | ], 269 | "zlib": [ 270 | "jspm_packages/npm/jspm-nodelibs-zlib@0.2.3" 271 | ], 272 | "zlib/*": [ 273 | "jspm_packages/npm/jspm-nodelibs-zlib@0.2.3/*", 274 | "jspm_packages/npm/jspm-nodelibs-zlib@0.2.3/*/index" 275 | ], 276 | "zone.js": [ 277 | "jspm_packages/npm/zone.js@0.9.1" 278 | ], 279 | "zone.js/*": [ 280 | "jspm_packages/npm/zone.js@0.9.1/*", 281 | "jspm_packages/npm/zone.js@0.9.1/*/index" 282 | ], 283 | "ts-runtime": [ 284 | "jspm_packages/npm/babel-runtime@6.26.0" 285 | ], 286 | "ts-runtime/*": [ 287 | "jspm_packages/npm/babel-runtime@6.26.0/*", 288 | "jspm_packages/npm/babel-runtime@6.26.0/*/index" 289 | ], 290 | "core-js": [ 291 | "jspm_packages/npm/core-js@3.1.3" 292 | ], 293 | "core-js/*": [ 294 | "jspm_packages/npm/core-js@3.1.3/*", 295 | "jspm_packages/npm/core-js@3.1.3/*/index" 296 | ], 297 | "plugin-babel": [ 298 | "jspm_packages/npm/systemjs-plugin-babel@0.0.25" 299 | ], 300 | "plugin-babel/*": [ 301 | "jspm_packages/npm/systemjs-plugin-babel@0.0.25/*", 302 | "jspm_packages/npm/systemjs-plugin-babel@0.0.25/*/index" 303 | ], 304 | "text": [ 305 | "jspm_packages/github/systemjs/plugin-text@0.0.11" 306 | ], 307 | "text/*": [ 308 | "jspm_packages/github/systemjs/plugin-text@0.0.11/*", 309 | "jspm_packages/github/systemjs/plugin-text@0.0.11/*/index" 310 | ], 311 | "ts": [ 312 | "jspm_packages/github/frankwallis/plugin-typescript@9.0.0" 313 | ], 314 | "ts/*": [ 315 | "jspm_packages/github/frankwallis/plugin-typescript@9.0.0/*", 316 | "jspm_packages/github/frankwallis/plugin-typescript@9.0.0/*/index" 317 | ], 318 | "readline": [ 319 | "jspm_packages/npm/jspm-nodelibs-readline@0.2.1" 320 | ], 321 | "readline/*": [ 322 | "jspm_packages/npm/jspm-nodelibs-readline@0.2.1/*", 323 | "jspm_packages/npm/jspm-nodelibs-readline@0.2.1/*/index" 324 | ], 325 | "babel-runtime": [ 326 | "jspm_packages/npm/babel-runtime@6.26.0" 327 | ], 328 | "babel-runtime/*": [ 329 | "jspm_packages/npm/babel-runtime@6.26.0/*", 330 | "jspm_packages/npm/babel-runtime@6.26.0/*/index" 331 | ], 332 | "systemjs-plugin-babel": [ 333 | "jspm_packages/npm/systemjs-plugin-babel@0.0.25" 334 | ], 335 | "systemjs-plugin-babel/*": [ 336 | "jspm_packages/npm/systemjs-plugin-babel@0.0.25/*", 337 | "jspm_packages/npm/systemjs-plugin-babel@0.0.25/*/index" 338 | ], 339 | "frankwallis/plugin-typescript": [ 340 | "jspm_packages/github/frankwallis/plugin-typescript@9.0.0" 341 | ], 342 | "frankwallis/plugin-typescript/*": [ 343 | "jspm_packages/github/frankwallis/plugin-typescript@9.0.0/*", 344 | "jspm_packages/github/frankwallis/plugin-typescript@9.0.0/*/index" 345 | ], 346 | "systemjs/plugin-text": [ 347 | "jspm_packages/github/systemjs/plugin-text@0.0.11" 348 | ], 349 | "systemjs/plugin-text/*": [ 350 | "jspm_packages/github/systemjs/plugin-text@0.0.11/*", 351 | "jspm_packages/github/systemjs/plugin-text@0.0.11/*/index" 352 | ], 353 | "source-map-support": [ 354 | "jspm_packages/npm/source-map-support@0.5.12" 355 | ], 356 | "source-map-support/*": [ 357 | "jspm_packages/npm/source-map-support@0.5.12/*", 358 | "jspm_packages/npm/source-map-support@0.5.12/*/index" 359 | ] 360 | } 361 | } 362 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "forin": true, 8 | "indent": [true, "spaces"], 9 | "label-position": true, 10 | "max-line-length": [false, 140], 11 | "member-access": true, 12 | "member-ordering": [true, 13 | "public-before-private", 14 | "static-before-instance", 15 | "variables-before-functions" 16 | ], 17 | "no-arg": true, 18 | "no-bitwise": true, 19 | "no-console": [true, 20 | "debug", 21 | "info", 22 | "time", 23 | "timeEnd", 24 | "trace" 25 | ], 26 | "no-construct": true, 27 | "no-debugger": true, 28 | "no-duplicate-variable": true, 29 | "no-empty": false, 30 | "no-eval": true, 31 | "no-inferrable-types": false, 32 | "no-shadowed-variable": true, 33 | "no-string-literal": true, 34 | "no-switch-case-fall-through": true, 35 | "no-trailing-whitespace": true, 36 | "no-unused-expression": true, 37 | "no-unused-variable": false, 38 | "no-use-before-declare": false, 39 | "no-var-keyword": true, 40 | "object-literal-sort-keys": false, 41 | "one-line": [true, 42 | "check-open-brace", 43 | "check-catch", 44 | "check-else", 45 | "check-finally", 46 | "check-whitespace" 47 | ], 48 | "quotemark": [false, "double", "avoid-escape"], 49 | "radix": true, 50 | "semicolon": [true, "always"], 51 | "trailing-comma": [false, { 52 | "singleline": "never", 53 | "multiline": "always" 54 | }], 55 | "triple-equals": [true, "allow-null-check"], 56 | "typedef-whitespace": [true, { 57 | "call-signature": "nospace", 58 | "index-signature": "nospace", 59 | "parameter": "nospace", 60 | "property-declaration": "nospace", 61 | "variable-declaration": "nospace" 62 | }], 63 | "variable-name": false, 64 | "whitespace": [true, 65 | "check-branch", 66 | "check-decl", 67 | "check-operator", 68 | "check-separator", 69 | "check-type" 70 | ] 71 | } 72 | } 73 | --------------------------------------------------------------------------------