├── .editorconfig ├── .firebaserc ├── .gitignore ├── .stylelintrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── circle.yml ├── codecov.yml ├── data ├── security.rules.json ├── seed.settings.json └── seed.superusers.json ├── docs ├── angular-universal.md ├── api-server.md └── openid-server.md ├── e2e-spec.json ├── firebase.json ├── fuse.ts ├── package-lock.json ├── package.json ├── src ├── client │ ├── app │ │ ├── __snapshots__ │ │ │ └── app.component.spec.ts.snap │ │ ├── about │ │ │ ├── __snapshots__ │ │ │ │ └── about.component.spec.ts.snap │ │ │ ├── about-routing.module.ts │ │ │ ├── about.component.e2e-spec.ts │ │ │ ├── about.component.html │ │ │ ├── about.component.scss │ │ │ ├── about.component.spec.ts │ │ │ ├── about.component.ts │ │ │ └── about.module.ts │ │ ├── account │ │ │ ├── account-routing.module.ts │ │ │ ├── account.component.html │ │ │ ├── account.component.scss │ │ │ ├── account.component.ts │ │ │ └── account.module.ts │ │ ├── admin │ │ │ ├── __snapshots__ │ │ │ │ └── admin.component.spec.ts.snap │ │ │ ├── admin-routing.module.ts │ │ │ ├── admin.component.e2e-spec.ts │ │ │ ├── admin.component.html │ │ │ ├── admin.component.scss │ │ │ ├── admin.component.spec.ts │ │ │ ├── admin.component.ts │ │ │ └── admin.module.ts │ │ ├── app-routing.module.ts │ │ ├── app.browser.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.module.ts │ │ ├── changelog │ │ │ ├── __snapshots__ │ │ │ │ └── changelog.component.spec.ts.snap │ │ │ ├── changelog-routing.module.ts │ │ │ ├── changelog.component.e2e-spec.ts │ │ │ ├── changelog.component.html │ │ │ ├── changelog.component.scss │ │ │ ├── changelog.component.spec.ts │ │ │ ├── changelog.component.ts │ │ │ └── changelog.module.ts │ │ ├── dashboard │ │ │ ├── dashboard-menu.ts │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.ts │ │ │ ├── dashboard.module.ts │ │ │ ├── footer │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── dashboard-page-footer.component.spec.ts.snap │ │ │ │ ├── dashboard-page-footer.component.html │ │ │ │ ├── dashboard-page-footer.component.scss │ │ │ │ ├── dashboard-page-footer.component.spec.ts │ │ │ │ └── dashboard-page-footer.component.ts │ │ │ ├── header │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── dashboard-page-header.component.spec.ts.snap │ │ │ │ ├── dashboard-page-header.component.html │ │ │ │ ├── dashboard-page-header.component.scss │ │ │ │ ├── dashboard-page-header.component.spec.ts │ │ │ │ └── dashboard-page-header.component.ts │ │ │ ├── test │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── test.component.spec.ts.snap │ │ │ │ ├── test-routing.module.ts │ │ │ │ ├── test.component.html │ │ │ │ ├── test.component.scss │ │ │ │ ├── test.component.spec.ts │ │ │ │ ├── test.component.ts │ │ │ │ ├── test.module.ts │ │ │ │ ├── test.service.ts │ │ │ │ ├── testChild │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── testChild.component.spec.ts.snap │ │ │ │ │ ├── testChild.component.html │ │ │ │ │ ├── testChild.component.scss │ │ │ │ │ ├── testChild.component.spec.ts │ │ │ │ │ └── testChild.component.ts │ │ │ │ └── testChild1 │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── testChild1.component.spec.ts.snap │ │ │ │ │ ├── testChild1.component.html │ │ │ │ │ ├── testChild1.component.scss │ │ │ │ │ ├── testChild1.component.spec.ts │ │ │ │ │ └── testChild1.component.ts │ │ │ └── test1 │ │ │ │ ├── __snapshots__ │ │ │ │ └── test1.component.spec.ts.snap │ │ │ │ ├── test1-routing.module.ts │ │ │ │ ├── test1.component.html │ │ │ │ ├── test1.component.scss │ │ │ │ ├── test1.component.spec.ts │ │ │ │ ├── test1.component.ts │ │ │ │ └── test1.module.ts │ │ ├── home │ │ │ ├── __snapshots__ │ │ │ │ └── home.component.spec.ts.snap │ │ │ ├── home-routing.module.ts │ │ │ ├── home.component.e2e-spec.ts │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ └── home.module.ts │ │ ├── login │ │ │ ├── __snapshots__ │ │ │ │ └── login.component.spec.ts.snap │ │ │ ├── login-routing.module.ts │ │ │ ├── login.component.e2e-spec.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ ├── login.component.ts │ │ │ └── login.module.ts │ │ ├── logout │ │ │ ├── __snapshots__ │ │ │ │ └── logout.component.spec.ts.snap │ │ │ ├── logout-routing.module.ts │ │ │ ├── logout.component.e2e-spec.ts │ │ │ ├── logout.component.html │ │ │ ├── logout.component.scss │ │ │ ├── logout.component.spec.ts │ │ │ ├── logout.component.ts │ │ │ └── logout.module.ts │ │ ├── not-found │ │ │ ├── __snapshots__ │ │ │ │ └── not-found.component.spec.ts.snap │ │ │ ├── not-found-routing.module.ts │ │ │ ├── not-found.component.e2e-spec.ts │ │ │ ├── not-found.component.html │ │ │ ├── not-found.component.scss │ │ │ ├── not-found.component.spec.ts │ │ │ ├── not-found.component.ts │ │ │ └── not-found.module.ts │ │ ├── pages │ │ │ ├── page-form │ │ │ │ ├── page-form.component.html │ │ │ │ ├── page-form.component.scss │ │ │ │ └── page-form.component.ts │ │ │ ├── pages-routing.module.ts │ │ │ ├── pages.component.html │ │ │ ├── pages.component.scss │ │ │ ├── pages.component.ts │ │ │ └── pages.module.ts │ │ ├── shared │ │ │ ├── cache-form │ │ │ │ ├── cache-form.component.html │ │ │ │ ├── cache-form.component.scss │ │ │ │ └── cache-form.component.ts │ │ │ ├── directives │ │ │ │ ├── avatar.directive.ts │ │ │ │ ├── click-outside.directive.ts │ │ │ │ ├── html-outlet.directive.ts │ │ │ │ └── social-button.directive.ts │ │ │ ├── http-cache-tag │ │ │ │ ├── http-cache-tag-interceptor.service.spec.ts │ │ │ │ ├── http-cache-tag-interceptor.service.ts │ │ │ │ └── http-cache-tag.module.ts │ │ │ ├── injection-form │ │ │ │ ├── injection-form.component.html │ │ │ │ ├── injection-form.component.scss │ │ │ │ └── injection-form.component.ts │ │ │ ├── key-value-form │ │ │ │ ├── key-value-form.component.html │ │ │ │ ├── key-value-form.component.scss │ │ │ │ └── key-value-form.component.ts │ │ │ ├── login-card │ │ │ │ ├── login-card.component.html │ │ │ │ ├── login-card.component.scss │ │ │ │ └── login-card.component.ts │ │ │ ├── material.module.ts │ │ │ ├── modal-confirmation │ │ │ │ ├── modal-confirmation.component.html │ │ │ │ ├── modal-confirmation.component.scss │ │ │ │ └── modal-confirmation.component.ts │ │ │ ├── navbar │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── navbar.component.spec.ts.snap │ │ │ │ ├── navbar.component.html │ │ │ │ ├── navbar.component.scss │ │ │ │ ├── navbar.component.spec.ts │ │ │ │ ├── navbar.component.ts │ │ │ │ ├── navbar.service.spec.ts │ │ │ │ └── navbar.service.ts │ │ │ ├── pipes │ │ │ │ ├── key-value.pipe.ts │ │ │ │ ├── keys.pipe.ts │ │ │ │ └── sanitize-html.pipe.ts │ │ │ ├── quill-editor │ │ │ │ ├── quill-editor.component.html │ │ │ │ ├── quill-editor.component.scss │ │ │ │ └── quill-editor.component.ts │ │ │ ├── services │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── error-handler.service.spec.ts.snap │ │ │ │ ├── adblock.service.spec.ts │ │ │ │ ├── adblock.service.ts │ │ │ │ ├── auth.service.ts │ │ │ │ ├── cookie.service.spec.ts │ │ │ │ ├── cookie.service.ts │ │ │ │ ├── environment.service.spec.ts │ │ │ │ ├── environment.service.ts │ │ │ │ ├── error-handler.service.spec.ts │ │ │ │ ├── error-handler.service.ts │ │ │ │ ├── firebase-database.service.ts │ │ │ │ ├── guard-admin.service.ts │ │ │ │ ├── guard-login.service.ts │ │ │ │ ├── http-config-interceptor.service.spec.ts │ │ │ │ ├── http-config-interceptor.service.ts │ │ │ │ ├── http-cookie-interceptor.service.spec.ts │ │ │ │ ├── http-cookie-interceptor.service.ts │ │ │ │ ├── injection.service.ts │ │ │ │ ├── logging.service.spec.ts │ │ │ │ ├── logging.service.ts │ │ │ │ ├── minifier.service.ts │ │ │ │ ├── platform.service.spec.ts │ │ │ │ ├── platform.service.ts │ │ │ │ ├── seo.service.ts │ │ │ │ ├── server-response.service.spec.ts │ │ │ │ ├── server-response.service.ts │ │ │ │ ├── setting.service.spec.ts │ │ │ │ ├── setting.service.ts │ │ │ │ └── web-socket.service.ts │ │ │ ├── shared.module.ts │ │ │ ├── style-injection-form │ │ │ │ ├── style-injection-form.component.html │ │ │ │ ├── style-injection-form.component.scss │ │ │ │ └── style-injection-form.component.ts │ │ │ └── web-app-installer │ │ │ │ ├── web-app-installer.component.html │ │ │ │ ├── web-app-installer.component.scss │ │ │ │ ├── web-app-installer.component.ts │ │ │ │ ├── web-app-installer.module.ts │ │ │ │ └── web-app-installer.service.ts │ │ ├── signup │ │ │ ├── __snapshots__ │ │ │ │ └── signup.component.spec.ts.snap │ │ │ ├── signup-routing.module.ts │ │ │ ├── signup.component.e2e-spec.ts │ │ │ ├── signup.component.html │ │ │ ├── signup.component.scss │ │ │ ├── signup.component.spec.ts │ │ │ ├── signup.component.ts │ │ │ └── signup.module.ts │ │ ├── unauthorized │ │ │ ├── __snapshots__ │ │ │ │ └── unauthorized.component.spec.ts.snap │ │ │ ├── unauthorized-routing.module.ts │ │ │ ├── unauthorized.component.html │ │ │ ├── unauthorized.component.scss │ │ │ ├── unauthorized.component.spec.ts │ │ │ ├── unauthorized.component.ts │ │ │ └── unauthorized.module.ts │ │ └── users │ │ │ ├── __snapshots__ │ │ │ └── users.component.spec.ts.snap │ │ │ ├── users-routing.module.ts │ │ │ ├── users.component.html │ │ │ ├── users.component.scss │ │ │ ├── users.component.spec.ts │ │ │ ├── users.component.ts │ │ │ └── users.module.ts │ ├── assets │ │ ├── angular-white-transparent.svg │ │ └── favicon.png │ ├── index.html │ ├── main-prod.ts │ ├── main.aot-prod.ts │ ├── main.aot.ts │ ├── main.ts │ ├── ngsw.json │ ├── operators.ts │ ├── polyfills.ts │ └── styles │ │ ├── main.scss │ │ └── material2-app-themes.scss ├── server │ ├── api │ │ ├── api.spec.ts │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── settings.controller.spec.ts │ │ │ └── settings.controller.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── index.ts │ │ │ └── zone-error-handler.ts │ │ ├── repositories │ │ │ ├── index.ts │ │ │ ├── setting.repository.ts │ │ │ └── settings.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ └── setting.service.ts │ │ └── test-helper.ts │ ├── server.angular-fire.service.ts │ ├── server.angular.module.ts │ ├── server.config.ts │ ├── server.sitemap.ts │ ├── server.ts │ ├── server.web-socket.ts │ └── service-account.json ├── testing │ ├── app-testing.module.ts │ ├── mock-cookie.service.ts │ ├── mock-environment.service.ts │ └── mock-firebase-database.service.ts ├── tsconfig.json └── tslint.json ├── tools ├── config │ ├── app.config.ts │ ├── build.ci.replace.ts │ ├── build.config.ts │ ├── build.interfaces.ts │ └── console.banner.256.txt ├── env │ ├── base.ts │ ├── dev.ts │ ├── e2e.ts │ └── prod.ts ├── manual-typings │ ├── README.md │ ├── project │ │ └── sample.package.d.ts │ └── seed │ │ ├── bunyan.d.ts │ │ ├── hash-files.d.ts │ │ ├── json.d.ts │ │ └── loglevel-std-streams.d.ts ├── plugins │ ├── ng-aot.ts │ ├── pwa-fused.ts │ └── web-index.ts ├── scripts │ ├── post-merge.sh │ └── replace.ts ├── tasks │ ├── index.ts │ ├── project │ │ └── sample.ts │ └── seed │ │ ├── assets.ts │ │ ├── banner.ts │ │ ├── changelog.ts │ │ ├── clean.ts │ │ ├── config.ts │ │ ├── favicons.ts │ │ ├── fonts.ts │ │ ├── index.copy.ts │ │ ├── index.minify.ts │ │ ├── lint.ts │ │ ├── mk-dist.ts │ │ ├── ngc.ts │ │ ├── ngsw-json.ts │ │ ├── ngsw-worker.min.ts │ │ ├── ngsw-worker.ts │ │ ├── ngsw.ts │ │ ├── sass.files.ts │ │ ├── sass.ts │ │ ├── serve.ts │ │ └── web.ts ├── test │ ├── jest.e2e-setup.ts │ ├── jest.mocks.ts │ └── jest.setup.ts ├── tslint-rules │ └── validateDecoratorsRule.ts └── web │ ├── ping.html │ └── robots.txt ├── tsconfig-aot.json ├── tsconfig-e2e.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "fuse-angular-universal-s-67402" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .DS_Store 8 | dist 9 | .env 10 | .fusebox 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | dist/ 65 | 66 | .DS_Store 67 | 68 | test-report.xml 69 | test-results.xml 70 | 71 | ngc 72 | .ngc 73 | .aot 74 | aot 75 | .e2e 76 | 77 | documentation 78 | 79 | src/config.json -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-no-empty": null, 4 | "color-no-invalid-hex": true, 5 | "comment-empty-line-before": [ 6 | "always", { 7 | "ignore": ["stylelint-commands", "between-comments"] 8 | } 9 | ], 10 | "declaration-colon-space-after": "always", 11 | "indentation": 2, 12 | "max-empty-lines": 2, 13 | "unit-whitelist": ["em", "rem", "%", "px", "s", "ms", "vw", "vh", "deg"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "msjsdiag.debugger-for-chrome", 4 | "michelemelluso.code-beautifier", 5 | "eg2.tslint", 6 | "eamodio.gitlens" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 2, 4 | "beautify.tabSize": 2, 5 | "editor.insertSpaces": true, 6 | "autoimport.filesToScan": "src/**/*.{ts,tsx}", 7 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone. By participating in this project, you 4 | agree to abide by our [code of conduct]. 5 | 6 | [code of conduct]: https://github.com/patrickmichalina/fusebox-angular-universal-starter/blob/master/CODE_OF_CONDUCT.md 7 | 8 | Fork, then clone the repo: 9 | 10 | git clone https://github.com/patrickmichalina/fusebox-angular-universal-starter 11 | 12 | Make sure the tests pass: 13 | 14 | npm test 15 | 16 | Make your change. Add tests for your change. Make the tests pass: 17 | 18 | npm test 19 | 20 | Push to your fork and [submit a pull request][pr]. 21 | 22 | [pr]: https://github.com/patrickmichalina/fusebox-angular-universal-starter/compare 23 | 24 | At this point you're waiting on us. We like to at least comment on pull requests 25 | within three business days (and, typically, one business day). We may suggest 26 | some changes or improvements or alternatives. 27 | 28 | Some things that will increase the chance that your pull request is accepted: 29 | 30 | * Write tests. 31 | * Write a [good commit message][commit]. 32 | 33 | [commit]: https://conventionalcommits.org 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Patrick Michalina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/server.js --prod -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fusebox-angular-universal-star", 3 | "env": { 4 | "CI": { 5 | "required": true 6 | }, 7 | "HOST": { 8 | "required": true 9 | }, 10 | "HEROKU": { 11 | "required": true 12 | }, 13 | "NODE_MODULES_CACHE": { 14 | "required": true 15 | }, 16 | "NPM_CONFIG_PRODUCTION": { 17 | "required": true 18 | }, 19 | "HEROKU_APP_NAME": { 20 | "required": true 21 | }, 22 | "HEROKU_PARENT_APP_NAME": { 23 | "required": true 24 | }, 25 | "FB_SERVICE_ACCOUNT_PRIVATE_KEY": { 26 | "required": true 27 | }, 28 | "FB_SERVICE_ACCOUNT_PRIVATE_KEY_ID": { 29 | "required": true 30 | }, 31 | "FB_AUTH_KEY": { 32 | "required": true 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 9.4.0 4 | npm: 5 | version: 5.6.0 6 | 7 | general: 8 | artifacts: 9 | - ./coverage 10 | - ./dist 11 | 12 | test: 13 | override: 14 | - npm run lint 15 | - npm run gen.config 16 | - npm run test.ci.before 17 | - node_modules/.bin/jest --ci --updateSnapshot --runInBand --coverage: 18 | environment: 19 | TEST_REPORT_PATH: $CIRCLE_TEST_REPORTS 20 | TEST_REPORT_FILENAME: test-results.xml 21 | - npm run test.ci.after 22 | 23 | post: 24 | - bash <(curl -s https://codecov.io/bash) 25 | - npm run semantic-release || true 26 | 27 | deployment: 28 | production: 29 | branch: master 30 | heroku: 31 | appname: fusebox-angular-universal-star -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | allow_coverage_offsets: false 3 | ignore: 4 | - "tools" -------------------------------------------------------------------------------- /data/security.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "site-settings": { 4 | ".read": "true", 5 | ".write": "root.child('users').child(auth.uid).child('roles/superadmin').val() === true" 6 | }, 7 | "posts": { 8 | ".read": "true", 9 | ".write": "true" 10 | }, 11 | "pages": { 12 | ".read": "true", 13 | ".write": "true", 14 | "blog": { 15 | "$document": { 16 | ".read": "true", 17 | ".write": "true" 18 | } 19 | } 20 | }, 21 | "users": { 22 | ".read": "auth != null && root.child('users').child(auth.uid).child('roles/superadmin').val() === true", 23 | "$uid": { 24 | ".read": "auth != null && auth.uid == $uid || root.child('users').child(auth.uid).child('roles/superadmin').val() === true", 25 | ".write": "auth != null && auth.uid == $uid || root.child('users').child(auth.uid).child('roles/superadmin').val() === true" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /data/seed.superusers.json: -------------------------------------------------------------------------------- 1 | { 2 | "9BthHoxklbgCzP2vlFI3ZrNggrl1": { 3 | "roles": { 4 | "superadmin": true, 5 | "admin": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /docs/api-server.md: -------------------------------------------------------------------------------- 1 | # API 2 | This project includes an example API that can be split off into a standalone server. The goal of this API is to provide a set of commonly used endpoints that could apply to most modern web applications. This inclides things like: 3 | 4 | - Site Settings 5 | - Image Uploads 6 | - Tokens (login/logout) 7 | 8 | # Stack 9 | - [routing-controllers](https://github.com/pleerock/routing-controllers) Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage in Express / Koa using TypeScript and Routing Controllers Framework. 10 | - [swagger-jsdoc]() Generates swagger doc based on JSDoc. 11 | - [swagger-ui-express]() Adds middleware to your express app to serve the Swagger UI bound to your Swagger document. This acts as living documentation for your API hosted from within your app. 12 | - [typedi]() Dependency Injection for TypeScript. 13 | 14 | # Structure 15 | The API has an opinionted structure in order to better sperate the various layers of the applications: 16 | 17 | - Controllers: gateway to the outside world, where users actually interact with the server 18 | - Services: internal tools that can be used in controllers, other services, or anywhere in the API 19 | - Repositories: data stores that are normally accessed via services 20 | - Middlewares: express server plugins 21 | 22 | ## Removing from the project 23 | ... TODO -------------------------------------------------------------------------------- /docs/openid-server.md: -------------------------------------------------------------------------------- 1 | # Identity Server 2 | This project includes an OpenID certifiec identity server that can be split off into a standalone OpenID server. 3 | 4 | ## Removing from the project 5 | ... TODO -------------------------------------------------------------------------------- /e2e-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "js", 5 | "html" 6 | ], 7 | "testRegex": "(/__tests__/.*|\\.(test|e2e-spec))\\.(ts|js)$", 8 | "transform": { 9 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 10 | }, 11 | "globals": { 12 | "__TRANSFORM_HTML__": true, 13 | "__process_env__": {}, 14 | "ts-jest": { 15 | "tsConfigFile": "tsconfig.json" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "data/security.rules.json" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/client/app/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { AboutComponent } from './about.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: AboutComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.about.title', 16 | description: 'i18n.about.description' 17 | }, 18 | response: { 19 | cache: { 20 | directive: 'no-cache' 21 | } 22 | } 23 | } 24 | } 25 | ]) 26 | ], 27 | exports: [RouterModule] 28 | }) 29 | export class AboutRoutingModule { } 30 | -------------------------------------------------------------------------------- /src/client/app/about/about.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('About Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | const page = browser.goto(`${baseUrl}/about`) 7 | 8 | const text = await page.evaluate(() => document.title) 9 | 10 | expect(text).toContain('About - Fusebox Angular Universal Starter') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/client/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |

About

2 |

An Angular starter with all the tools necessary to create server rendered Angular applications.

3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 17 |
18 | 19 | 20 |

{{ post.title }}

21 |
22 |
-------------------------------------------------------------------------------- /src/client/app/about/about.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/about/about.component.scss -------------------------------------------------------------------------------- /src/client/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AboutComponent } from './about.component' 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 3 | import { Component } from '@angular/core' 4 | import { AboutModule } from './about.module' 5 | import { AppTestingModule } from '../../../testing/app-testing.module' 6 | 7 | @Component({ 8 | selector: 'test-component', 9 | template: '' 10 | }) 11 | class TestComponent {} 12 | 13 | describe(AboutComponent.name, () => { 14 | let fixture: ComponentFixture 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [AppTestingModule.forRoot(), AboutModule], 19 | declarations: [TestComponent] 20 | }).compileComponents() 21 | })) 22 | 23 | beforeEach(async(() => { 24 | fixture = TestBed.createComponent(TestComponent) 25 | })) 26 | 27 | afterEach(async(() => { 28 | TestBed.resetTestingModule() 29 | })) 30 | 31 | it('should compile', async(() => { 32 | fixture.detectChanges() 33 | expect(fixture.nativeElement).toBeDefined() 34 | expect(fixture).toMatchSnapshot() 35 | })) 36 | }) 37 | -------------------------------------------------------------------------------- /src/client/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { PlatformService } from './../shared/services/platform.service' 2 | import { ChangeDetectionStrategy, Component } from '@angular/core' 3 | import { FirebaseDatabaseService } from '../shared/services/firebase-database.service' 4 | import { FormControl, FormGroup, Validators } from '@angular/forms' 5 | 6 | @Component({ 7 | selector: 'pm-about', 8 | templateUrl: './about.component.html', 9 | styleUrls: ['./about.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class AboutComponent { 13 | readonly posts$ = this.db.getList<{ readonly title: string, readonly html: string }>('posts').map(a => { 14 | return a.map((b: any) => { 15 | return { 16 | title: b.title || '', 17 | html: b.html || '' 18 | } 19 | }) 20 | }) 21 | 22 | public readonly form = new FormGroup({ 23 | title: new FormControl('', [ 24 | Validators.required 25 | ]), 26 | html: new FormControl('', [ 27 | Validators.required 28 | ]) 29 | }) 30 | 31 | constructor(private db: FirebaseDatabaseService, ps: PlatformService) { } 32 | 33 | create() { 34 | this.db.getListRef('posts').push(this.form.value) 35 | } 36 | 37 | removeAll() { 38 | this.db.getObjectRef('posts').remove() 39 | } 40 | 41 | trackByPost(index: number, item: any) { 42 | return index 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/client/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { AboutRoutingModule } from './about-routing.module' 2 | import { AboutComponent } from './about.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [AboutRoutingModule, SharedModule], 8 | declarations: [AboutComponent], 9 | exports: [AboutComponent] 10 | }) 11 | export class AboutModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/account/account-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { AccountComponent } from './account.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | import { LoginGuard } from '../shared/services/guard-login.service' 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: '', 12 | component: AccountComponent, 13 | canActivate: [MetaGuard, LoginGuard], 14 | data: { 15 | meta: { 16 | title: 'i18n.account.title', 17 | description: 'i18n.account.description' 18 | }, 19 | response: { 20 | cache: { 21 | directive: 'private' 22 | } 23 | } 24 | } 25 | } 26 | ]) 27 | ], 28 | exports: [RouterModule] 29 | }) 30 | export class AccountRoutingModule { } 31 | -------------------------------------------------------------------------------- /src/client/app/account/account.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .flex-center { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | img { 7 | cursor: pointer; 8 | } 9 | } 10 | mat-accordion { 11 | margin-top: 1em; 12 | } 13 | form { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | #update-btn { 18 | width: 100%; 19 | &:hover { 20 | cursor: pointer; 21 | } 22 | } 23 | .headers-align .mat-expansion-panel-header-title, 24 | .headers-align .mat-expansion-panel-header-description { 25 | flex-basis: 0; 26 | } 27 | .headers-align .mat-expansion-panel-header-description { 28 | justify-content: space-between; 29 | align-items: center; 30 | } 31 | .avatar { 32 | height: 95px; 33 | width: 95px; 34 | } 35 | .social-spacer { 36 | display: flex; 37 | flex-direction: row; 38 | align-items: baseline; 39 | } 40 | } -------------------------------------------------------------------------------- /src/client/app/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { AccountRoutingModule } from './account-routing.module' 2 | import { AccountComponent } from './account.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [AccountRoutingModule, SharedModule], 8 | declarations: [AccountComponent], 9 | exports: [AccountComponent] 10 | }) 11 | export class AccountModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/admin/__snapshots__/admin.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AdminComponent should match snapshot 1`] = ` 4 | 5 |

6 | Admin 7 |

8 | Your Admin module is routed here 9 |

10 |
11 | `; 12 | -------------------------------------------------------------------------------- /src/client/app/admin/admin-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { AdminComponent } from './admin.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: AdminComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.admin.title', 16 | description: 'i18n.admin.description' 17 | }, 18 | response: { 19 | cache: { 20 | directive: 'private' 21 | } 22 | } 23 | } 24 | } 25 | ]) 26 | ], 27 | exports: [RouterModule] 28 | }) 29 | export class AdminRoutingModule { } 30 | -------------------------------------------------------------------------------- /src/client/app/admin/admin.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('Admin Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | const page = browser.goto(`${baseUrl}/admin`) 7 | 8 | const text = await page.evaluate(() => document.title) 9 | 10 | expect(text).toEqual('Admin - Fusebox Angular Universal Starter') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/client/app/admin/admin.component.html: -------------------------------------------------------------------------------- 1 |

Admin

2 |

Your Admin module is routed here

-------------------------------------------------------------------------------- /src/client/app/admin/admin.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/admin/admin.component.scss -------------------------------------------------------------------------------- /src/client/app/admin/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AdminComponent } from './admin.component' 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 3 | import { Component } from '@angular/core' 4 | import { AdminModule } from './admin.module' 5 | import { AppTestingModule } from '../../../testing/app-testing.module' 6 | 7 | describe(AdminComponent.name, () => { 8 | let fixture: ComponentFixture 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [AppTestingModule.forRoot(), AdminModule], 13 | declarations: [TestComponent] 14 | }).compileComponents() 15 | })) 16 | 17 | beforeEach(async(() => { 18 | fixture = TestBed.createComponent(AdminComponent) 19 | })) 20 | 21 | afterEach(async(() => { 22 | TestBed.resetTestingModule() 23 | })) 24 | 25 | it('should match snapshot', async(() => { 26 | expect(fixture).toMatchSnapshot() 27 | })) 28 | 29 | it('should compile', async(() => { 30 | expect(fixture.nativeElement).toBeDefined() 31 | })) 32 | }) 33 | 34 | @Component({ 35 | selector: 'test-component', 36 | template: '' 37 | }) 38 | class TestComponent {} 39 | -------------------------------------------------------------------------------- /src/client/app/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-admin', 5 | templateUrl: './admin.component.html', 6 | styleUrls: ['./admin.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class AdminComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { AdminRoutingModule } from './admin-routing.module' 2 | import { AdminComponent } from './admin.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [AdminRoutingModule, SharedModule], 8 | declarations: [AdminComponent], 9 | exports: [AdminComponent] 10 | }) 11 | export class AdminModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | export const routes: Routes = [ 5 | { path: '', loadChildren: async () => (await import('./home/home.module')).HomeModule }, 6 | { path: 'unauthorized', loadChildren: async () => (await import('./unauthorized/unauthorized.module')).UnauthorizedModule }, 7 | { path: 'about', loadChildren: async () => (await import('./about/about.module')).AboutModule }, 8 | { path: 'account', loadChildren: async () => (await import('./account/account.module')).AccountModule }, 9 | { path: 'login', loadChildren: async () => (await import('./login/login.module')).LoginModule }, 10 | { path: 'logout', loadChildren: async () => (await import('./logout/logout.module')).LogoutModule }, 11 | { path: 'signup', loadChildren: async () => (await import('./signup/signup.module')).SignupModule }, 12 | { path: 'admin', loadChildren: async () => (await import('./admin/admin.module')).AdminModule }, 13 | { path: 'changelog', loadChildren: async () => (await import('./changelog/changelog.module')).ChangelogModule }, 14 | { path: 'dashboard', loadChildren: async () => (await import('./dashboard/dashboard.module')).DashboardModule }, 15 | { path: 'pages', loadChildren: async () => (await import('./pages/pages.module')).PagesModule }, 16 | { path: 'users', loadChildren: async () => (await import('./users/users.module')).UsersModule } 17 | ] 18 | 19 | @NgModule({ 20 | imports: [ 21 | RouterModule.forRoot(routes, { initialNavigation: 'enabled' }) 22 | ], 23 | exports: [RouterModule] 24 | }) 25 | export class AppRoutingModule { } 26 | -------------------------------------------------------------------------------- /src/client/app/app.browser.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 2 | import { BrowserModule, BrowserTransferStateModule, TransferState } from '@angular/platform-browser' 3 | import { AppModule, REQ_KEY } from './app.module' 4 | import { NgModule } from '@angular/core' 5 | import { AppComponent } from './app.component' 6 | import { REQUEST } from '@nguniversal/express-engine/tokens' 7 | import 'hammerjs' 8 | 9 | export function getRequest(transferState: TransferState): any { 10 | return transferState.get(REQ_KEY, {}) 11 | } 12 | 13 | @NgModule({ 14 | bootstrap: [AppComponent], 15 | imports: [ 16 | BrowserModule.withServerTransition({ appId: 'pm-app' }), 17 | BrowserTransferStateModule, 18 | BrowserAnimationsModule, 19 | // ServiceWorkerModule.register('/ngsw-worker.js'), 20 | AppModule 21 | ], 22 | providers: [ 23 | { 24 | provide: REQUEST, 25 | useFactory: getRequest, 26 | deps: [TransferState] 27 | } 28 | ] 29 | }) 30 | export class AppBrowserModule { } 31 | -------------------------------------------------------------------------------- /src/client/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Home 11 | Changelog 12 | Sitemap 13 | Robots 14 | Consulting 15 | 16 | 17 |

Boss Area

18 | 19 | Pages 20 | Assets 21 | Users 22 | Admin 23 | 24 |
25 |
26 | 27 |
-------------------------------------------------------------------------------- /src/client/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1 0; 4 | flex-direction: column; 5 | .sidenav-container { 6 | overflow-y: auto; 7 | flex: 1 0; 8 | flex-direction: column; 9 | } 10 | .sidenav { 11 | width: 256px; 12 | } 13 | #menu-top { 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | mat-slide-toggle { 18 | margin-right: 1em; 19 | } 20 | } 21 | ::ng-deep mat-sidenav-container { 22 | mat-sidenav { 23 | visibility: hidden; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/client/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core' 2 | import { EnvConfig } from '../../../tools/config/app.config' 3 | 4 | export const ENV_CONFIG = new InjectionToken('app.config') 5 | -------------------------------------------------------------------------------- /src/client/app/changelog/__snapshots__/changelog.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ChangelogComponent should compile 1`] = ` 4 | 5 | 6 | 9 |
10 | 11 | 12 | 13 | `; 14 | -------------------------------------------------------------------------------- /src/client/app/changelog/changelog-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { ChangelogComponent } from './changelog.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: ChangelogComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.changelog.title', 16 | description: 'i18n.changelog.description' 17 | } 18 | } 19 | } 20 | ]) 21 | ], 22 | exports: [RouterModule] 23 | }) 24 | export class ChangelogRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/client/app/changelog/changelog.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('Changelog Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | 7 | const page = browser.goto(`${baseUrl}/changelog`) 8 | const text = await page.evaluate(() => document.title) 9 | 10 | expect(text).toContain('Changelog - Fusebox Angular Universal Starter') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/client/app/changelog/changelog.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
-------------------------------------------------------------------------------- /src/client/app/changelog/changelog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/changelog/changelog.component.scss -------------------------------------------------------------------------------- /src/client/app/changelog/changelog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ChangelogComponent } from './changelog.component' 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 3 | import { Component } from '@angular/core' 4 | import { ChangelogModule } from './changelog.module' 5 | import { AppTestingModule } from '../../../testing/app-testing.module' 6 | 7 | @Component({ 8 | selector: 'test-component', 9 | template: '' 10 | }) 11 | class TestComponent { } 12 | 13 | describe(ChangelogComponent.name, () => { 14 | let fixture: ComponentFixture 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [AppTestingModule.forRoot(), ChangelogModule], 19 | declarations: [TestComponent] 20 | }).compileComponents() 21 | })) 22 | 23 | beforeEach(async(() => { 24 | fixture = TestBed.createComponent(TestComponent) 25 | })) 26 | 27 | afterEach(async(() => { 28 | TestBed.resetTestingModule() 29 | })) 30 | 31 | it('should compile', async(() => { 32 | expect(fixture.nativeElement).toBeDefined() 33 | expect(fixture).toMatchSnapshot() 34 | })) 35 | }) 36 | -------------------------------------------------------------------------------- /src/client/app/changelog/changelog.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { HttpClient } from '@angular/common/http' 3 | import { ChangeDetectionStrategy, Component } from '@angular/core' 4 | 5 | @Component({ 6 | selector: 'pm-changelog', 7 | templateUrl: './changelog.component.html', 8 | styleUrls: ['./changelog.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ChangelogComponent { 12 | readonly changelog$ = this.http.get('./changelog.md', { responseType: 'text' }) 13 | .catch(a => Observable.of('Error loading changelog.md')) 14 | 15 | constructor(private http: HttpClient) { } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/app/changelog/changelog.module.ts: -------------------------------------------------------------------------------- 1 | import { ChangelogComponent } from './changelog.component' 2 | import { ChangelogRoutingModule } from './changelog-routing.module' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [ChangelogRoutingModule, SharedModule], 8 | declarations: [ChangelogComponent], 9 | exports: [ChangelogComponent] 10 | }) 11 | export class ChangelogModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard-menu.ts: -------------------------------------------------------------------------------- 1 | export const DASHBOARD_MENU = [ 2 | { 3 | heading: 'Heading 1', 4 | children: [ 5 | { 6 | path: 'test/test-child', 7 | title: 'Test Child' 8 | }, 9 | { 10 | path: 'test/test-child-1', 11 | title: 'Test Child 1' 12 | } 13 | 14 | ] 15 | }, 16 | { 17 | heading: 'Heading 2', 18 | children: [ 19 | { 20 | path: 'test1', 21 | title: 'Test 1 Page' 22 | } 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule } from '@angular/router' 3 | import { DashboardComponent } from './dashboard.component' 4 | 5 | @NgModule({ 6 | imports: [ 7 | RouterModule.forChild([ 8 | { 9 | path: '', 10 | component: DashboardComponent, 11 | children: [ 12 | { path: '', redirectTo: 'test', pathMatch: 'full' }, 13 | { 14 | path: 'test', 15 | loadChildren: async () => (await import('./test/test.module')).TestModule 16 | }, 17 | { 18 | path: 'test1', 19 | loadChildren: async () => (await import('./test1/test1.module')).Test1Module 20 | } 21 | ] 22 | } 23 | ]) 24 | ], 25 | exports: [RouterModule] 26 | }) 27 | export class DashboardRoutingModule { } 28 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 22 | 32 | 42 | 43 |
44 | 45 | 46 | 47 |
48 |
-------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Title } from '@angular/platform-browser' 2 | import { ChangeDetectionStrategy, Component, HostListener, OnInit, ViewChild } from '@angular/core' 3 | import { MatSidenav } from '@angular/material' 4 | import { DASHBOARD_MENU } from './dashboard-menu' 5 | 6 | @Component({ 7 | selector: 'pm-dashboard', 8 | templateUrl: './dashboard.component.html', 9 | styleUrls: ['./dashboard.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class DashboardComponent implements OnInit { 13 | 14 | pageTitle: string 15 | 16 | readonly isMobile = false 17 | 18 | readonly dashboardMenu = DASHBOARD_MENU 19 | 20 | @ViewChild('sidenav') public readonly sidenav: MatSidenav 21 | 22 | @HostListener('window:resize', ['$event']) 23 | onResize(event: any) { 24 | if (event.target.innerWidth < 840) { 25 | this.sidenav.close() 26 | } 27 | if (event.target.innerWidth >= 840) { 28 | this.sidenav.open() 29 | } 30 | } 31 | 32 | constructor(private title: Title) { } 33 | 34 | ngOnInit() { 35 | const title = this.title.getTitle() 36 | this.setPageTitle(title.substr(0, title.indexOf('-'))) 37 | } 38 | 39 | toggleSidenav(event: any) { 40 | this.sidenav.toggle() 41 | } 42 | 43 | setPageTitle(title: string) { 44 | this.pageTitle = title 45 | } 46 | 47 | trackByMenu(index: number, item: any) { 48 | return index 49 | } 50 | 51 | trackByChild(index: number, item: any) { 52 | return index 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/client/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { DashboardComponent } from './dashboard.component' 2 | import { DashboardRoutingModule } from './dashboard-routing.module' 3 | import { DashboardPageHeaderComponent } from './header/dashboard-page-header.component' 4 | import { DashboardPageFooterComponent } from './footer/dashboard-page-footer.component' 5 | import { NgModule } from '@angular/core' 6 | import { SharedModule } from '../shared/shared.module' 7 | import { MatIconModule, MatSidenavModule } from '@angular/material' 8 | 9 | @NgModule({ 10 | imports: [DashboardRoutingModule, SharedModule, MatSidenavModule, MatIconModule], 11 | declarations: [DashboardComponent, DashboardPageHeaderComponent, DashboardPageFooterComponent], 12 | exports: [DashboardComponent, DashboardPageHeaderComponent, DashboardPageFooterComponent] 13 | }) 14 | export class DashboardModule { } 15 | -------------------------------------------------------------------------------- /src/client/app/dashboard/footer/__snapshots__/dashboard-page-footer.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DashboardPageFooterComponent should match snapshot 1`] = ` 4 | 5 |
8 | 41 |
42 |
43 | `; 44 | -------------------------------------------------------------------------------- /src/client/app/dashboard/footer/dashboard-page-footer.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
-------------------------------------------------------------------------------- /src/client/app/dashboard/footer/dashboard-page-footer.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .dashboard-footer { 3 | margin-top: 40px; 4 | padding: 12px; 5 | font-size: 12px; 6 | background: #2196f3; 7 | color: rgba(255, 255, 255, 0.87); 8 | .dashboard-footer-list { 9 | -webkit-box-align: center; 10 | -ms-flex-align: center; 11 | align-items: center; 12 | display: -webkit-box; 13 | display: -ms-flexbox; 14 | display: flex; 15 | -webkit-box-orient: horizontal; 16 | -webkit-box-direction: normal; 17 | -ms-flex-flow: row wrap; 18 | flex-flow: row wrap; 19 | padding: 8px; 20 | } 21 | .logo { 22 | height: 50px; 23 | } 24 | .dashboard-footer-links { 25 | ul { 26 | list-style: none; 27 | margin: 0 40px; 28 | padding: 0; 29 | a { 30 | font-size: 16px; 31 | padding: 0; 32 | text-decoration: none; 33 | color: rgba(255, 255, 255, 0.87); 34 | } 35 | } 36 | } 37 | .dashboard-footer-copyright { 38 | display: -webkit-box; 39 | display: -ms-flexbox; 40 | display: flex; 41 | -webkit-box-flex: 1; 42 | -ms-flex: 1; 43 | flex: 1; 44 | -webkit-box-pack: end; 45 | -ms-flex-pack: end; 46 | justify-content: flex-end; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/client/app/dashboard/footer/dashboard-page-footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { Component } from '@angular/core' 3 | import { DashboardPageFooterComponent } from './dashboard-page-footer.component' 4 | import { DashboardModule } from '../dashboard.module' 5 | 6 | describe(DashboardPageFooterComponent.name, () => { 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [DashboardModule], 12 | declarations: [TestComponent] 13 | }).compileComponents() 14 | })) 15 | 16 | beforeEach(async(() => { 17 | fixture = TestBed.createComponent(DashboardPageFooterComponent) 18 | })) 19 | 20 | afterEach(async(() => { 21 | TestBed.resetTestingModule() 22 | })) 23 | 24 | it('should match snapshot', async(() => { 25 | expect(fixture).toMatchSnapshot() 26 | })) 27 | 28 | it('should compile', async(() => { 29 | expect(fixture.nativeElement).toBeDefined() 30 | })) 31 | }) 32 | 33 | @Component({ 34 | selector: 'test-component', 35 | template: '' 36 | }) 37 | class TestComponent {} 38 | -------------------------------------------------------------------------------- /src/client/app/dashboard/footer/dashboard-page-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-dashboard-page-footer', 5 | templateUrl: './dashboard-page-footer.component.html', 6 | styleUrls: ['./dashboard-page-footer.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class DashboardPageFooterComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/client/app/dashboard/header/__snapshots__/dashboard-page-header.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DashboardPageHeaderComponent should match snapshot 1`] = ` 4 | 7 |
10 | 35 |

36 | 37 |

38 |
39 |
40 | `; 41 | -------------------------------------------------------------------------------- /src/client/app/dashboard/header/dashboard-page-header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |

{{ title }}

10 |
-------------------------------------------------------------------------------- /src/client/app/dashboard/header/dashboard-page-header.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .docs-primary-header { 3 | background: #9ba0a5; 4 | padding-left: 20px; 5 | display: -webkit-box; 6 | display: -ms-flexbox; 7 | display: flex; 8 | -webkit-box-align: center; 9 | -ms-flex-align: center; 10 | align-items: center; 11 | h1 { 12 | font-size: 30px; 13 | font-weight: 300; 14 | margin: 0; 15 | padding: 20px 20px 20px 10px; 16 | color: hsla(0, 0%, 100%, .87); 17 | } 18 | } 19 | .side-nav-toggle { 20 | padding-top: 14px; 21 | margin: 8px; 22 | min-width: 64px; 23 | display: none; 24 | } 25 | .mat-icon { 26 | color: white; 27 | } 28 | @media (max-width: 840px) { 29 | .side-nav-toggle { 30 | display: block; 31 | } 32 | .docs-primary-header { 33 | padding-left: 0px; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/client/app/dashboard/header/dashboard-page-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { Component } from '@angular/core' 3 | import { DashboardModule } from '../dashboard.module' 4 | import { DashboardPageHeaderComponent } from './dashboard-page-header.component' 5 | 6 | describe(DashboardPageHeaderComponent.name, () => { 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [DashboardModule], 12 | declarations: [TestComponent] 13 | }).compileComponents() 14 | })) 15 | 16 | beforeEach(async(() => { 17 | fixture = TestBed.createComponent(DashboardPageHeaderComponent) 18 | })) 19 | 20 | afterEach(async(() => { 21 | TestBed.resetTestingModule() 22 | })) 23 | 24 | it('should match snapshot', async(() => { 25 | expect(fixture).toMatchSnapshot() 26 | })) 27 | 28 | it('should compile', async(() => { 29 | expect(fixture.nativeElement).toBeDefined() 30 | })) 31 | }) 32 | 33 | @Component({ 34 | selector: 'test-component', 35 | template: '' 36 | }) 37 | class TestComponent {} 38 | -------------------------------------------------------------------------------- /src/client/app/dashboard/header/dashboard-page-header.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-dashboard-page-header', 5 | templateUrl: './dashboard-page-header.component.html', 6 | styleUrls: ['./dashboard-page-header.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class DashboardPageHeaderComponent { 10 | 11 | @Input() readonly title: string 12 | 13 | @Output() readonly toggleSidenav: EventEmitter = new EventEmitter() 14 | 15 | toggle() { 16 | this.toggleSidenav.emit(true) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/__snapshots__/test.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TestComponent should match snapshot 1`] = ` 4 | 5 | 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { TestComponent } from './test.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | import { TestChild1Component } from './testChild1/testChild1.component' 6 | import { TestChildComponent } from './testChild/testChild.component' 7 | 8 | @NgModule({ 9 | imports: [ 10 | RouterModule.forChild([ 11 | { 12 | path: '', 13 | component: TestComponent, 14 | canActivateChild: [MetaGuard], 15 | children: [ 16 | { path: '', redirectTo: 'test-child', pathMatch: 'full' }, 17 | { 18 | path: 'test-child', 19 | component: TestChildComponent, 20 | data: { 21 | meta: { 22 | title: 'Test Child', 23 | // tslint:disable-next-line:max-line-length 24 | description: 'Test for angular related projects on github, to showcase the flicker-free http state transfer of an Angular isomorphic application.' 25 | } 26 | } 27 | }, 28 | { 29 | path: 'test-child-1', 30 | component: TestChild1Component, 31 | data: { 32 | meta: { 33 | title: 'Test Child 1 Page', 34 | // tslint:disable-next-line:max-line-length 35 | description: 'Test for angular related projects on github, to showcase the flicker-free http state transfer of an Angular isomorphic application.' 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | ]) 42 | ], 43 | exports: [RouterModule] 44 | }) 45 | export class TestRoutingModule { } 46 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/dashboard/test/test.component.scss -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { Component } from '@angular/core' 3 | import { TestModule } from './test.module' 4 | import { TestComponent } from './test.component' 5 | import { RouterTestingModule } from '@angular/router/testing' 6 | import { TestChildComponent } from './testChild/testChild.component' 7 | import { TestChild1Component } from './testChild1/testChild1.component' 8 | import { Route } from '@angular/router' 9 | import { EnvironmentService } from '../../shared/services/environment.service' 10 | import { APP_BASE_HREF } from '@angular/common' 11 | 12 | describe(TestComponent.name, () => { 13 | const config: Array = [ 14 | { 15 | path: '', 16 | component: TestComponent, 17 | pathMatch: 'full', 18 | children: [ 19 | { path: '', redirectTo: 'test-child', pathMatch: 'full' }, 20 | { 21 | path: 'test-child', 22 | component: TestChildComponent 23 | }, 24 | { 25 | path: 'test-child-1', 26 | component: TestChild1Component 27 | } 28 | ] 29 | } 30 | ] 31 | 32 | let fixture: ComponentFixture 33 | 34 | beforeEach(async(() => { 35 | TestBed.configureTestingModule({ 36 | imports: [TestModule, RouterTestingModule.withRoutes(config)], 37 | declarations: [TestingComponent], 38 | providers: [ 39 | { provide: APP_BASE_HREF, useValue: '/' }, 40 | EnvironmentService 41 | ] 42 | }).compileComponents() 43 | })) 44 | 45 | beforeEach(async(() => { 46 | fixture = TestBed.createComponent(TestComponent) 47 | })) 48 | 49 | afterEach(async(() => { 50 | TestBed.resetTestingModule() 51 | })) 52 | 53 | it('should match snapshot', async(() => { 54 | expect(fixture).toMatchSnapshot() 55 | })) 56 | 57 | it('should compile', async(() => { 58 | expect(fixture.nativeElement).toBeDefined() 59 | })) 60 | }) 61 | 62 | @Component({ 63 | selector: 'test-component', 64 | template: '' 65 | }) 66 | class TestingComponent {} 67 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-test', 5 | templateUrl: './test.component.html', 6 | styleUrls: ['./test.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class TestComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test.module.ts: -------------------------------------------------------------------------------- 1 | import { TestRoutingModule } from './test-routing.module' 2 | import { TestComponent } from './test.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../../shared/shared.module' 5 | import { TestChildComponent } from './testChild/testChild.component' 6 | import { TestChild1Component } from './testChild1/testChild1.component' 7 | 8 | @NgModule({ 9 | imports: [TestRoutingModule, SharedModule], 10 | declarations: [TestComponent, TestChild1Component, TestChildComponent], 11 | exports: [TestComponent, TestChild1Component, TestChildComponent] 12 | }) 13 | export class TestModule { } 14 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/test.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | 3 | @Injectable() 4 | export class TestService { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild/__snapshots__/testChild.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TestChildComponent should match snapshot 1`] = ` 4 | 5 | 8 | Test Child Project 9 | 10 | 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild/testChild.component.html: -------------------------------------------------------------------------------- 1 | 2 | Test Child Project 3 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild/testChild.component.scss: -------------------------------------------------------------------------------- 1 | .mat-card { 2 | margin: 10px; 3 | height: 400px; 4 | } -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild/testChild.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { Component } from '@angular/core' 3 | import { TestChildComponent } from './testChild.component' 4 | import { TestModule } from '../test.module' 5 | 6 | describe(TestChildComponent.name, () => { 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [TestModule], 12 | declarations: [TestComponent] 13 | }).compileComponents() 14 | })) 15 | 16 | beforeEach(async(() => { 17 | fixture = TestBed.createComponent(TestChildComponent) 18 | })) 19 | 20 | afterEach(async(() => { 21 | TestBed.resetTestingModule() 22 | })) 23 | 24 | it('should match snapshot', async(() => { 25 | expect(fixture).toMatchSnapshot() 26 | })) 27 | 28 | it('should compile', async(() => { 29 | expect(fixture.nativeElement).toBeDefined() 30 | })) 31 | }) 32 | 33 | @Component({ 34 | selector: 'test-component', 35 | template: '' 36 | }) 37 | class TestComponent {} 38 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild/testChild.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-test-child-1', 5 | templateUrl: './testChild.component.html', 6 | styleUrls: ['./testChild.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class TestChildComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild1/__snapshots__/testChild1.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TestChild1Component should match snapshot 1`] = ` 4 | 5 | 8 | Test Child 1 Project 9 | 10 | 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild1/testChild1.component.html: -------------------------------------------------------------------------------- 1 | 2 | Test Child 1 Project 3 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild1/testChild1.component.scss: -------------------------------------------------------------------------------- 1 | .mat-card { 2 | margin: 10px; 3 | height: 400px; 4 | } -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild1/testChild1.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { Component } from '@angular/core' 3 | import { TestModule } from '../test.module' 4 | import { TestChild1Component } from './testChild1.component' 5 | 6 | describe(TestChild1Component.name, () => { 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [TestModule], 12 | declarations: [TestComponent] 13 | }).compileComponents() 14 | })) 15 | 16 | beforeEach(async(() => { 17 | fixture = TestBed.createComponent(TestChild1Component) 18 | })) 19 | 20 | afterEach(async(() => { 21 | TestBed.resetTestingModule() 22 | })) 23 | 24 | it('should match snapshot', async(() => { 25 | expect(fixture).toMatchSnapshot() 26 | })) 27 | 28 | it('should compile', async(() => { 29 | expect(fixture.nativeElement).toBeDefined() 30 | })) 31 | }) 32 | 33 | @Component({ 34 | selector: 'test-component', 35 | template: '' 36 | }) 37 | class TestComponent {} 38 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test/testChild1/testChild1.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-test-child-2', 5 | templateUrl: './testChild1.component.html', 6 | styleUrls: ['./testChild1.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class TestChild1Component { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/__snapshots__/test1.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test1Component should match snapshot 1`] = ` 4 | 5 | 8 | Test 1 Project 9 | 10 | 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/test1-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Test1Component } from './test1.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: Test1Component, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'Test 1 Page', 16 | // tslint:disable-next-line:max-line-length 17 | description: 'Test 1 for angular related projects on github, to showcase the flicker-free http state transfer of an Angular isomorphic application.' 18 | } 19 | } 20 | } 21 | ]) 22 | ], 23 | exports: [RouterModule] 24 | }) 25 | export class Test1RoutingModule { } 26 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/test1.component.html: -------------------------------------------------------------------------------- 1 | 2 | Test 1 Project 3 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/test1.component.scss: -------------------------------------------------------------------------------- 1 | .mat-card { 2 | margin: 10px; 3 | height: 400px; 4 | } -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/test1.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { Component } from '@angular/core' 3 | import { Test1Component } from './test1.component' 4 | import { Test1Module } from './test1.module' 5 | 6 | describe(Test1Component.name, () => { 7 | let fixture: ComponentFixture 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [Test1Module], 12 | declarations: [TestComponent] 13 | }).compileComponents() 14 | })) 15 | 16 | beforeEach(async(() => { 17 | fixture = TestBed.createComponent(Test1Component) 18 | })) 19 | 20 | afterEach(async(() => { 21 | TestBed.resetTestingModule() 22 | })) 23 | 24 | it('should match snapshot', async(() => { 25 | expect(fixture).toMatchSnapshot() 26 | })) 27 | 28 | it('should compile', async(() => { 29 | expect(fixture.nativeElement).toBeDefined() 30 | })) 31 | }) 32 | 33 | @Component({ 34 | selector: 'test-component', 35 | template: '' 36 | }) 37 | class TestComponent {} 38 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/test1.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-test1', 5 | templateUrl: './test1.component.html', 6 | styleUrls: ['./test1.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class Test1Component { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/dashboard/test1/test1.module.ts: -------------------------------------------------------------------------------- 1 | import { Test1RoutingModule } from './test1-routing.module' 2 | import { Test1Component } from './test1.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [Test1RoutingModule, SharedModule], 8 | declarations: [Test1Component], 9 | exports: [Test1Component] 10 | }) 11 | export class Test1Module { } 12 | -------------------------------------------------------------------------------- /src/client/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { HomeComponent } from './home.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: HomeComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.home.title', 16 | description: 'i18n.home.description' 17 | }, 18 | response: { 19 | cache: { 20 | directive: 'public', 21 | maxage: '7d', 22 | smaxage: '7d' 23 | }, 24 | headers: { 25 | 'X-Cool-Tag': 'some-value' 26 | } 27 | } 28 | } 29 | } 30 | ]) 31 | ], 32 | exports: [RouterModule] 33 | }) 34 | export class HomeRoutingModule { } 35 | -------------------------------------------------------------------------------- /src/client/app/home/home.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('Home Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | 7 | const page = browser.goto(`${baseUrl}`) 8 | 9 | const text = await page.evaluate(() => document.title) 10 | 11 | expect(text).toContain('Home - Fusebox Angular Universal Starter') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/client/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | #hero { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | h1 { 7 | margin-bottom: 0; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { By } from '@angular/platform-browser' 2 | import { Angulartics2 } from 'angulartics2' 3 | import { HomeComponent } from './home.component' 4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 5 | import { Component } from '@angular/core' 6 | import { HomeModule } from './home.module' 7 | import { MatRaisedButtonCssMatStyler } from '@angular/material' 8 | import { AppTestingModule } from '../../../testing/app-testing.module' 9 | 10 | describe(HomeComponent.name, () => { 11 | let fixture: ComponentFixture 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [AppTestingModule.forRoot(), HomeModule], 16 | declarations: [TestComponent] 17 | }).compileComponents() 18 | })) 19 | 20 | beforeEach(async(() => { 21 | fixture = TestBed.createComponent(HomeComponent) 22 | })) 23 | 24 | afterEach(async(() => { 25 | TestBed.resetTestingModule() 26 | })) 27 | 28 | it('should compile', async(() => { 29 | expect(fixture.nativeElement).toBeDefined() 30 | expect(fixture).toMatchSnapshot() 31 | })) 32 | 33 | it('should track event when link clicked', async(() => { 34 | const analytics = TestBed.get(Angulartics2) as Angulartics2 35 | expect(analytics).toBeDefined() 36 | const button = fixture.debugElement.query(By.directive(MatRaisedButtonCssMatStyler)) 37 | expect(button).toBeTruthy() 38 | const link = button.nativeElement as HTMLAnchorElement 39 | expect(link).toBeTruthy() 40 | analytics.eventTrack.subscribe(eventTrack => { 41 | expect(eventTrack).toBeDefined() 42 | expect(eventTrack.action).toEqual('ViewRepo') 43 | expect(eventTrack.properties.eventType).toEqual('click') 44 | }) 45 | link.click() 46 | })) 47 | }) 48 | 49 | @Component({ 50 | selector: 'test-component', 51 | template: '' 52 | }) 53 | class TestComponent { } 54 | -------------------------------------------------------------------------------- /src/client/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class HomeComponent { 10 | @HostBinding('class.card-float-container') containerClass = true 11 | } 12 | -------------------------------------------------------------------------------- /src/client/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { HomeRoutingModule } from './home-routing.module' 2 | import { HomeComponent } from './home.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [HomeRoutingModule, SharedModule], 8 | declarations: [HomeComponent], 9 | exports: [HomeComponent] 10 | }) 11 | export class HomeModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/login/__snapshots__/login.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LoginComponent should compile 1`] = ` 4 | 5 |
9 |

10 | Login 11 |

12 | 13 | 14 | 15 | 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /src/client/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { LoginComponent } from './login.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: LoginComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.login.title', 16 | description: 'i18n.login.description' 17 | } 18 | } 19 | } 20 | ]) 21 | ], 22 | exports: [RouterModule] 23 | }) 24 | export class LoginRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/client/app/login/login.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('Login Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | 7 | const page = browser.goto(`${baseUrl}/login`) 8 | 9 | const text = await page.evaluate(() => document.title) 10 | 11 | expect(text).toContain('Login - Fusebox Angular Universal Starter') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/client/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Login

3 | 4 |
-------------------------------------------------------------------------------- /src/client/app/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/login/login.component.scss -------------------------------------------------------------------------------- /src/client/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './../shared/services/auth.service' 2 | import { AngularFireAuthModule } from 'angularfire2/auth' 3 | import { LoginComponent } from './login.component' 4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 5 | import { Component } from '@angular/core' 6 | import { LoginModule } from './login.module' 7 | import { AngularFireModule } from 'angularfire2' 8 | import { AppTestingModule } from '../../../testing/app-testing.module' 9 | 10 | describe(LoginComponent.name, () => { 11 | let fixture: ComponentFixture 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [AppTestingModule.forRoot(), LoginModule, AngularFireModule.initializeApp({ 16 | 'apiKey': '1', 17 | 'authDomain': 'app.firebaseapp.com', 18 | 'databaseURL': 'https://app.firebaseio.com', 19 | 'projectId': 'firebase-app', 20 | 'messagingSenderId': '1' 21 | }), AngularFireAuthModule], 22 | declarations: [TestComponent], 23 | providers: [ 24 | { provide: AuthService, userValue: { }} 25 | ] 26 | }).compileComponents() 27 | })) 28 | 29 | beforeEach(async(() => { 30 | fixture = TestBed.createComponent(LoginComponent) 31 | })) 32 | 33 | afterEach(async(() => { 34 | TestBed.resetTestingModule() 35 | })) 36 | 37 | it('should compile', async(() => { 38 | expect(fixture.nativeElement).toBeDefined() 39 | expect(fixture).toMatchSnapshot() 40 | })) 41 | }) 42 | 43 | @Component({ 44 | selector: 'test-component', 45 | template: '' 46 | }) 47 | class TestComponent {} 48 | -------------------------------------------------------------------------------- /src/client/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-login', 5 | templateUrl: './login.component.html', 6 | styleUrls: ['./login.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class LoginComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { LoginComponent } from './login.component' 2 | import { LoginRoutingModule } from './login-routing.module' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [LoginRoutingModule, SharedModule], 8 | declarations: [LoginComponent], 9 | exports: [LoginComponent] 10 | }) 11 | export class LoginModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/logout/__snapshots__/logout.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LogoutComponent should compile 1`] = ` 4 | 5 | You have been logged out. Come back soon! 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/app/logout/logout-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { LogoutComponent } from './logout.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: LogoutComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.logout.title', 16 | description: 'i18n.logout.description' 17 | } 18 | } 19 | } 20 | ]) 21 | ], 22 | exports: [RouterModule] 23 | }) 24 | export class LogoutRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/client/app/logout/logout.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('Logout Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | 7 | const page = browser.goto(`${baseUrl}/logout`) 8 | 9 | const text = await page.evaluate(() => document.title) 10 | 11 | expect(text).toContain('Logged Out - Fusebox Angular Universal Starter') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/client/app/logout/logout.component.html: -------------------------------------------------------------------------------- 1 | You have been logged out. Come back soon! -------------------------------------------------------------------------------- /src/client/app/logout/logout.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/logout/logout.component.scss -------------------------------------------------------------------------------- /src/client/app/logout/logout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AngularFireAuthModule } from 'angularfire2/auth' 2 | import { LogoutComponent } from './logout.component' 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 4 | import { Component } from '@angular/core' 5 | import { LogoutModule } from './logout.module' 6 | import { AngularFireModule } from 'angularfire2' 7 | import { AppTestingModule } from '../../../testing/app-testing.module' 8 | 9 | describe(LogoutComponent.name, () => { 10 | let fixture: ComponentFixture 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [AppTestingModule.forRoot(), LogoutModule, AngularFireAuthModule, AngularFireModule.initializeApp({ 15 | 'apiKey': '1', 16 | 'authDomain': 'app.firebaseapp.com', 17 | 'databaseURL': 'https://app.firebaseio.com', 18 | 'projectId': 'firebase-app', 19 | 'messagingSenderId': '1' 20 | })], 21 | declarations: [TestComponent] 22 | }).compileComponents() 23 | })) 24 | 25 | beforeEach(async(() => { 26 | fixture = TestBed.createComponent(LogoutComponent) 27 | })) 28 | 29 | afterEach(async(() => { 30 | TestBed.resetTestingModule() 31 | })) 32 | 33 | it('should compile', async(() => { 34 | expect(fixture.nativeElement).toBeDefined() 35 | expect(fixture).toMatchSnapshot() 36 | })) 37 | }) 38 | 39 | @Component({ 40 | selector: 'test-component', 41 | template: '' 42 | }) 43 | class TestComponent { } 44 | -------------------------------------------------------------------------------- /src/client/app/logout/logout.component.ts: -------------------------------------------------------------------------------- 1 | import { PlatformService } from './../shared/services/platform.service' 2 | import { CookieService } from './../shared/services/cookie.service' 3 | import { AngularFireAuth } from 'angularfire2/auth' 4 | import { ChangeDetectionStrategy, Component } from '@angular/core' 5 | 6 | @Component({ 7 | selector: 'pm-logout', 8 | templateUrl: './logout.component.html', 9 | styleUrls: ['./logout.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class LogoutComponent { 13 | constructor(afAuth: AngularFireAuth, cs: CookieService, ps: PlatformService) { 14 | cs.remove('fbJwt') 15 | cs.remove('fbPhotoURL') 16 | cs.remove('fbProviderId') 17 | cs.remove('fbEmail') 18 | cs.remove('fbDisplayName') 19 | if (ps.isBrowser) { 20 | afAuth.auth.signOut() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/app/logout/logout.module.ts: -------------------------------------------------------------------------------- 1 | import { LogoutRoutingModule } from './logout-routing.module' 2 | import { LogoutComponent } from './logout.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [LogoutRoutingModule, SharedModule], 8 | declarations: [LogoutComponent], 9 | exports: [LogoutComponent] 10 | }) 11 | export class LogoutModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/not-found/__snapshots__/not-found.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NotFoundComponent should compile 1`] = ` 4 | 5 | 8 | 9 |
13 | 14 | 15 |
18 |
22 |
25 | 26 | 48 |
49 |
50 |
51 | 52 | 53 | `; 54 | -------------------------------------------------------------------------------- /src/client/app/not-found/not-found-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundComponent } from './not-found.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | 5 | @NgModule({ 6 | imports: [ 7 | RouterModule.forChild([ 8 | { 9 | path: '**', 10 | component: NotFoundComponent, 11 | data: { 12 | // meta: { 13 | // title: 'i18n.not-found.title', 14 | // description: 'i18n.not-found.description' 15 | // } 16 | } 17 | } 18 | ]) 19 | ], 20 | exports: [RouterModule] 21 | }) 22 | export class NotFoundRoutingModule { } 23 | -------------------------------------------------------------------------------- /src/client/app/not-found/not-found.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('NotFound Page', () => { 4 | it('should have title', async () => { 5 | const page = browser.goto(`${baseUrl}/not-found`) 6 | 7 | const text = await page.evaluate(() => document.title) 8 | 9 | expect(text).toContain('Not Found - Fusebox Angular Universal Starter') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/client/app/not-found/not-found.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .footer { 3 | text-align: right; 4 | padding: .75em; 5 | } 6 | ::ng-deep mat-tab-group { 7 | flex: 1; 8 | height: 100%; 9 | .mat-tab-body-wrapper { 10 | height: 100%; 11 | } 12 | mat-tab-body { 13 | .mat-tab-body-content { 14 | overflow: overlay; 15 | } // overflow-y: overlay; 16 | // .mat-tab-body-content { 17 | // overflow-y: overlay; 18 | // } 19 | } 20 | } 21 | .settings { 22 | display: flex; 23 | flex-direction: column; 24 | padding: 1em; 25 | height: 100%; 26 | overflow-y: auto; 27 | } 28 | ::ng-deep dynamic-html { 29 | display: -webkit-inline-box; 30 | overflow-y: auto; 31 | height: 100%; 32 | } 33 | ::ng-deep mat-form-field { 34 | padding-bottom: 1em; 35 | } 36 | .example-headers-align .mat-expansion-panel-header-title, 37 | .example-headers-align .mat-expansion-panel-header-description { 38 | flex-basis: 0; 39 | } 40 | .example-headers-align .mat-expansion-panel-header-description { 41 | justify-content: space-between; 42 | align-items: center; 43 | } 44 | .field-spacer { 45 | display: flex; 46 | flex: 1; 47 | mat-form-field { 48 | width: 100%; 49 | margin-right: 1em; 50 | } 51 | @media only screen and (max-width: 959px) { 52 | flex-direction: column; 53 | } 54 | } 55 | ::ng-deep mat-chip { 56 | mat-icon { 57 | height: auto; 58 | width: auto; 59 | margin-left: 8px; 60 | padding-bottom: 4px; 61 | &:hover { 62 | cursor: pointer; 63 | } 64 | } 65 | } 66 | .pad-tab { 67 | padding: 0 1em; 68 | } 69 | .inj-list { 70 | mat-list-item { 71 | margin: 1em 0; 72 | } 73 | mat-card { 74 | width: 100%; 75 | padding: 1em; 76 | } 77 | } 78 | .card-list { 79 | margin-bottom: 1em; 80 | display: flex; 81 | align-items: center; 82 | mat-card { 83 | width: 100%; 84 | display: inherit; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/client/app/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundComponent } from './not-found.component' 2 | import { AuthService } from './../shared/services/auth.service' 3 | import { of } from 'rxjs/observable/of' 4 | import { TransferState } from '@angular/platform-browser' 5 | import { ServerResponseService } from './../shared/services/server-response.service' 6 | import { Observable } from 'rxjs/Observable' 7 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 8 | import { Component } from '@angular/core' 9 | import { NotFoundModule } from './not-found.module' 10 | import { FirebaseDatabaseService } from '../shared/services/firebase-database.service' 11 | import { AppTestingModule } from '../../../testing/app-testing.module' 12 | 13 | @Component({ 14 | selector: 'test-component', 15 | template: '' 16 | }) 17 | class TestComponent { } 18 | 19 | describe(NotFoundComponent.name, () => { 20 | let fixture: ComponentFixture 21 | 22 | beforeEach(async(() => { 23 | TestBed.configureTestingModule({ 24 | imports: [NotFoundModule, AppTestingModule.forRoot()], 25 | declarations: [TestComponent], 26 | providers: [ 27 | ServerResponseService, 28 | TransferState, 29 | { 30 | provide: FirebaseDatabaseService, 31 | useValue: { 32 | get() { 33 | return of({}) 34 | } 35 | } 36 | }, 37 | { 38 | provide: AuthService, 39 | useValue: { 40 | user$: Observable.of({}) 41 | } 42 | } 43 | ] 44 | }).compileComponents() 45 | })) 46 | 47 | beforeEach(async(() => { 48 | fixture = TestBed.createComponent(TestComponent) 49 | })) 50 | 51 | afterEach(async(() => { 52 | TestBed.resetTestingModule() 53 | })) 54 | 55 | it('should compile', async(() => { 56 | fixture.detectChanges() 57 | expect(fixture.componentInstance).toBeDefined() 58 | expect(fixture).toMatchSnapshot() 59 | })) 60 | }) 61 | -------------------------------------------------------------------------------- /src/client/app/not-found/not-found.module.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundRoutingModule } from './not-found-routing.module' 2 | import { NotFoundComponent } from './not-found.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [NotFoundRoutingModule, SharedModule], 8 | declarations: [NotFoundComponent], 9 | exports: [NotFoundComponent] 10 | }) 11 | export class NotFoundModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/pages/page-form/page-form.component.html: -------------------------------------------------------------------------------- 1 |

New Page

2 |
3 | 4 | 5 | 6 | must contain a forward slash and name 7 | 8 | 9 | slug already taken 10 | 11 | 12 | slug is 13 | required 14 | 15 | 16 | 17 | 18 | 19 | 20 | title is 21 | required 22 | 23 | 24 | 25 | 26 | 27 | 28 | description is 29 | required 30 | 31 | 32 | 33 | 34 | Draft (not visible to public) 35 | 36 |
37 | 38 |
-------------------------------------------------------------------------------- /src/client/app/pages/page-form/page-form.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | h2 { 3 | margin: 0 0 1em 0; 4 | } 5 | form { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | } -------------------------------------------------------------------------------- /src/client/app/pages/pages-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { PagesComponent } from './pages.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: PagesComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | // meta: { 15 | // title: 'i18n.admin.title', 16 | // description: 'i18n.admin.description' 17 | // }, 18 | response: { 19 | cache: { 20 | directive: 'private' 21 | } 22 | } 23 | } 24 | } 25 | ]) 26 | ], 27 | exports: [RouterModule] 28 | }) 29 | export class PagesRoutingModule { } 30 | -------------------------------------------------------------------------------- /src/client/app/pages/pages.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Pages

4 | 7 |
8 | 9 | 10 | 11 | 12 | Slug 13 | 14 | {{ element.slug }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
-------------------------------------------------------------------------------- /src/client/app/pages/pages.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .flex { 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 0 1em; 7 | } 8 | } -------------------------------------------------------------------------------- /src/client/app/pages/pages.module.ts: -------------------------------------------------------------------------------- 1 | import { PagesRoutingModule } from './pages-routing.module' 2 | import { PagesComponent } from './pages.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | import { PageFormComponent } from './page-form/page-form.component' 6 | 7 | @NgModule({ 8 | imports: [PagesRoutingModule, SharedModule], 9 | declarations: [PagesComponent, PageFormComponent], 10 | exports: [PagesComponent], 11 | entryComponents: [PageFormComponent] 12 | }) 13 | export class PagesModule { } 14 | -------------------------------------------------------------------------------- /src/client/app/shared/cache-form/cache-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ dir.key }} 3 |
4 | {{ dir.key }} 5 | 6 | 7 | 8 |
9 |
-------------------------------------------------------------------------------- /src/client/app/shared/cache-form/cache-form.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | 3 | } -------------------------------------------------------------------------------- /src/client/app/shared/directives/avatar.directive.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/shared/directives/avatar.directive.ts -------------------------------------------------------------------------------- /src/client/app/shared/directives/click-outside.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core' 2 | 3 | @Directive({ 4 | selector: '[pmClickOutside]' 5 | }) 6 | export class ClickOutsideDirective { 7 | @Input() exclude: string 8 | @Output() pmClickOutside = new EventEmitter() 9 | 10 | constructor(private elementRef: ElementRef) { } 11 | 12 | @HostListener('document:click', ['$event', '$event.target']) 13 | public onClick(event: MouseEvent, targetElement: any): void { 14 | if (!targetElement) return 15 | 16 | const clickedInside = this.elementRef.nativeElement.contains(targetElement) 17 | const nodes = Array.from(document.querySelectorAll(this.exclude)) as Array 18 | const whitelisted = nodes.some(a => a.contains(targetElement)) 19 | 20 | if (!clickedInside && !whitelisted) { 21 | this.pmClickOutside.emit(true) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/client/app/shared/directives/social-button.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core' 2 | 3 | @Directive({ 4 | selector: '[pmSocialButton]' 5 | }) 6 | export class SocialButtonDirective implements OnInit { 7 | @Input() readonly pmSocialButton: string 8 | 9 | @HostListener('mouseenter') onMouseEnter() { 10 | this.elementRef.nativeElement.style.backgroundColor = this.getHover(this.pmSocialButton) 11 | } 12 | 13 | @HostListener('mouseleave') onMouseLeave() { 14 | this.setBaseSyles(this.pmSocialButton) 15 | } 16 | 17 | constructor(private elementRef: ElementRef) { } 18 | 19 | private readonly map: { readonly [key: string]: { readonly bg: string, readonly hover: string, readonly border: string } } = { 20 | facebook: { bg: '#4267b2', hover: '#2d4373', border: '#29487d' }, 21 | twitter: { bg: '#55acee', hover: '#2795e9', border: 'rgba(0, 0, 0, 0.2)' }, 22 | github: { bg: '#444', hover: '#2b2b2b', border: 'rgba(0, 0, 0, 0.2)' }, 23 | google: { bg: '#dd4b39', hover: '#c23321', border: 'rgba(0, 0, 0, 0.2)' } 24 | } 25 | 26 | ngOnInit() { 27 | this.setBaseSyles(this.pmSocialButton) 28 | } 29 | 30 | setBaseSyles(provider: string) { 31 | const val = this.map[provider] || {} 32 | this.elementRef.nativeElement.style.backgroundColor = val.bg 33 | this.elementRef.nativeElement.style.color = '#fff' 34 | this.elementRef.nativeElement.style['border-bottom'] = `1px solid ${val.border}` 35 | } 36 | 37 | getHover(provider: string) { 38 | const val = this.map[provider] || {} 39 | return val.hover 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/app/shared/http-cache-tag/http-cache-tag-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core' 2 | import { HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http' 3 | import { CACHE_TAG_CONFIG, CACHE_TAG_FACTORY, CacheFactory, CacheTagConfig } from './http-cache-tag.module' 4 | 5 | @Injectable() 6 | export class HttpCacheTagInterceptor implements HttpInterceptor { 7 | 8 | constructor( @Inject(CACHE_TAG_CONFIG) private config: CacheTagConfig, 9 | @Inject(CACHE_TAG_FACTORY) private factory: CacheFactory) { 10 | if (!config.headerKey) throw new Error('missing config.headerKey') 11 | if (!config.cacheableResponseCodes) throw new Error('missing config.cacheableResponseCodes') 12 | } 13 | 14 | isCacheableCode(code: number) { 15 | return this.config.cacheableResponseCodes.find(a => a === code) 16 | } 17 | 18 | isCacheableUrl(url: string | null) { 19 | if (!this.config.cacheableUrls || !url || url === null) return true 20 | return this.config.cacheableUrls.test(url) 21 | } 22 | 23 | intercept(req: HttpRequest, next: HttpHandler) { 24 | return next.handle(req).map(event => { 25 | if (event instanceof HttpResponse && this.isCacheableCode(event.status) && this.isCacheableUrl(event.url)) { 26 | this.factory(event, this.config) 27 | } 28 | return event 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/client/app/shared/http-cache-tag/http-cache-tag.module.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core' 2 | import { HTTP_INTERCEPTORS, HttpResponse } from '@angular/common/http' 3 | import { HttpCacheTagInterceptor } from './http-cache-tag-interceptor.service' 4 | 5 | export const CACHE_TAG_CONFIG = new InjectionToken('app.config.http.cachetag') 6 | export const CACHE_TAG_FACTORY = new InjectionToken('app.config.http.cachetag') 7 | 8 | export interface CacheTagConfig { 9 | readonly headerKey: string 10 | readonly cacheableResponseCodes: ReadonlyArray 11 | readonly cacheableUrls?: RegExp, 12 | } 13 | 14 | export type CacheFactory = (httpResponse: HttpResponse, config: CacheTagConfig) => void 15 | 16 | @NgModule() 17 | export class HttpCacheTagModule { 18 | static forRoot(configProvider: any, factoryProvider: any): ModuleWithProviders { 19 | return { 20 | ngModule: HttpCacheTagModule, 21 | providers: [ 22 | { 23 | provide: HttpCacheTagInterceptor, 24 | useClass: HttpCacheTagInterceptor, 25 | deps: [CACHE_TAG_CONFIG, CACHE_TAG_FACTORY] 26 | }, 27 | { provide: HTTP_INTERCEPTORS, useExisting: HttpCacheTagInterceptor, multi: true }, 28 | configProvider, 29 | factoryProvider 30 | ] 31 | } 32 | } 33 | 34 | constructor( @Optional() @SkipSelf() parentModule: HttpCacheTagModule) { 35 | if (parentModule) 36 | throw new Error('HttpCachTageModule already loaded. Import in root module only.') 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/app/shared/injection-form/injection-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | 8 |
9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |

{{ htmlString }}

17 | 23 |
-------------------------------------------------------------------------------- /src/client/app/shared/injection-form/injection-form.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } -------------------------------------------------------------------------------- /src/client/app/shared/injection-form/injection-form.component.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2 } from '@angular/core' 3 | import { FormControl, FormGroup, Validators } from '@angular/forms' 4 | import { DOMInjectable, InjectionService } from '../services/injection.service' 5 | import { Subscription } from 'rxjs/Subscription' 6 | 7 | @Component({ 8 | selector: 'pm-injection-form', 9 | templateUrl: './injection-form.component.html', 10 | styleUrls: ['./injection-form.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class InjectionFormComponent implements OnInit, OnDestroy { 14 | @Input() showDomString: boolean 15 | @Input() injectable: DOMInjectable = {} as any 16 | @Output() formChange = new BehaviorSubject(this.injectable) 17 | @Output() changeHtmlString = new EventEmitter() 18 | 19 | public form: FormGroup 20 | private sub = new Subscription() 21 | 22 | get htmlString() { 23 | return this.inj.getElementStringForm(this.renderer, this.formChange.getValue()) 24 | } 25 | 26 | constructor(private renderer: Renderer2, private inj: InjectionService) { } 27 | 28 | ngOnInit() { 29 | this.form = new FormGroup({ 30 | element: new FormControl(this.injectable.element, [Validators.required]), 31 | inHead: new FormControl(this.injectable.inHead || false, []), 32 | value: new FormControl(this.injectable.value || undefined, []), 33 | attributes: new FormControl(this.injectable.attributes || {}, []) 34 | }) 35 | this.sub = this.form.valueChanges.skip(1).subscribe(form => { 36 | this.formChange.next(form) 37 | this.changeHtmlString.next(this.htmlString) 38 | }) 39 | } 40 | 41 | ngOnDestroy() { 42 | this.sub.unsubscribe() 43 | } 44 | 45 | attributesChanged(attrs: any) { 46 | if (this.form && this.form.controls) this.form.controls['attributes'].setValue(attrs || {}) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/app/shared/key-value-form/key-value-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 |
-------------------------------------------------------------------------------- /src/client/app/shared/key-value-form/key-value-form.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } -------------------------------------------------------------------------------- /src/client/app/shared/key-value-form/key-value-form.component.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' 2 | import { ChangeDetectionStrategy, Component, Input, OnChanges, Output, SimpleChanges } from '@angular/core' 3 | import { FormControl, FormGroup, Validators } from '@angular/forms' 4 | 5 | @Component({ 6 | selector: 'pm-key-value-form', 7 | templateUrl: './key-value-form.component.html', 8 | styleUrls: ['./key-value-form.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class KeyValueFormComponent implements OnChanges { 12 | @Input() keyVals: { [key: string]: string | boolean | number } = {} 13 | @Output() attributesChanged = new BehaviorSubject(this.keyVals) 14 | 15 | public form = new FormGroup({ 16 | key: new FormControl('', [Validators.required]), 17 | value: new FormControl('', [Validators.required]) 18 | }) 19 | 20 | ngOnChanges(changes: SimpleChanges) { 21 | if (changes.keyVals) { 22 | this.attributesChanged.next(changes.keyVals.currentValue) 23 | } 24 | } 25 | 26 | add(obj: { key: string, value: string }) { 27 | this.attributesChanged.next({ 28 | ...this.attributesChanged.getValue(), 29 | [obj.key]: obj.value 30 | }) 31 | } 32 | 33 | remove(key: string) { 34 | this.attributesChanged.next({ 35 | ...Object.keys(this.attributesChanged.getValue()) 36 | .filter(k => k !== key) 37 | .reduce((a, c) => ({ ...a, [c]: this.attributesChanged.getValue()[c] }), {}) 38 | }) 39 | } 40 | 41 | trackByFn(index: number, item: any) { 42 | console.log(item) 43 | return index 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/client/app/shared/login-card/login-card.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | div { 3 | display: flex; 4 | flex-direction: column; 5 | button, 6 | p { 7 | align-self: center; 8 | width: 190px; 9 | margin-bottom: 1em; 10 | } 11 | } 12 | mat-expansion-panel { 13 | max-width: 300px; 14 | align-self: center; 15 | } 16 | form { 17 | mat-form-field { 18 | width: 100%; 19 | } 20 | } 21 | h1 { 22 | margin-top: 0; 23 | text-align: center; 24 | } 25 | #socialNetworkErr { 26 | max-width: 300px; 27 | margin-bottom: 2em; 28 | } 29 | .social-spacer { 30 | display: flex; 31 | flex-direction: row; 32 | align-items: baseline; 33 | } 34 | ::ng-deep .mat-expansion-panel-body { 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | button { 39 | margin-bottom: 1em; 40 | width: 200px; 41 | } 42 | } 43 | ::ng-deep mat-action-row { 44 | justify-content: space-between; 45 | padding: 8px; 46 | button { 47 | margin: 0 !important; 48 | } 49 | } 50 | } 51 | 52 | mat-icon { 53 | vertical-align: inherit !important; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/client/app/shared/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { 3 | MatButtonModule, MatCardModule, MatCheckboxModule, MatChipsModule, 4 | MatDatepickerModule, MatDialogModule, MatExpansionModule, MatExpansionPanel, MatFormFieldModule, 5 | MatIconModule, MatInputModule, MatListModule, MatMenuModule, 6 | MatNativeDateModule, MatPaginatorModule, MatProgressSpinnerModule, MatRippleModule, MatSelectModule, 7 | MatSidenavModule, MatSlideToggleModule, MatSnackBarModule, MatTableModule, MatTabsModule, 8 | MatTooltipModule, NativeDateAdapter 9 | } from '@angular/material' 10 | import { OverlayModule } from '@angular/cdk/overlay' 11 | 12 | @NgModule({ 13 | imports: [ 14 | MatButtonModule, 15 | MatCardModule, 16 | MatIconModule, 17 | MatSidenavModule, 18 | MatTooltipModule, 19 | MatFormFieldModule, 20 | MatInputModule, 21 | MatExpansionModule, 22 | MatMenuModule, 23 | MatProgressSpinnerModule, 24 | MatListModule, 25 | MatSnackBarModule, 26 | MatSlideToggleModule, 27 | MatTabsModule, 28 | MatTableModule, 29 | MatDialogModule, 30 | MatDatepickerModule, 31 | MatNativeDateModule, 32 | MatChipsModule, 33 | MatSelectModule, 34 | MatCheckboxModule, 35 | OverlayModule, 36 | MatRippleModule, 37 | MatPaginatorModule 38 | ], 39 | providers: [NativeDateAdapter], 40 | entryComponents: [MatExpansionPanel], 41 | exports: [ 42 | MatButtonModule, 43 | MatCardModule, 44 | MatIconModule, 45 | MatSidenavModule, 46 | MatTooltipModule, 47 | MatFormFieldModule, 48 | MatInputModule, 49 | MatExpansionModule, 50 | MatMenuModule, 51 | MatProgressSpinnerModule, 52 | MatListModule, 53 | MatSnackBarModule, 54 | MatSlideToggleModule, 55 | MatTabsModule, 56 | MatTableModule, 57 | MatDialogModule, 58 | MatDatepickerModule, 59 | MatNativeDateModule, 60 | MatChipsModule, 61 | MatSelectModule, 62 | MatCheckboxModule, 63 | OverlayModule, 64 | MatRippleModule, 65 | MatPaginatorModule 66 | ] 67 | }) 68 | export class MaterialModule { } 69 | -------------------------------------------------------------------------------- /src/client/app/shared/modal-confirmation/modal-confirmation.component.html: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 |

{{ message }}

3 | -------------------------------------------------------------------------------- /src/client/app/shared/modal-confirmation/modal-confirmation.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | h1,h2,h3,h4,h5 { 5 | margin: 0; 6 | } 7 | .footer { 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | } -------------------------------------------------------------------------------- /src/client/app/shared/modal-confirmation/modal-confirmation.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core' 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material' 3 | 4 | @Component({ 5 | selector: 'pm-modal-confirmation', 6 | templateUrl: './modal-confirmation.component.html', 7 | styleUrls: ['./modal-confirmation.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class ModalConfirmationComponent { 11 | @Input() readonly message: string 12 | @Input() readonly title: string 13 | 14 | constructor(public dialog: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) { } 15 | 16 | readonly view = { 17 | message: this.data.message || this.message, 18 | title: this.data.title || this.title 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/app/shared/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/client/app/shared/navbar/navbar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../node_modules/@angular/material/_theming.scss'; 2 | 3 | :host { 4 | z-index: 10; 5 | nav { 6 | background-color: mat-color($mat-blue, 700); 7 | display: -webkit-box; 8 | display: -ms-flexbox; 9 | display: flex; 10 | -ms-flex-wrap: wrap; 11 | flex-wrap: wrap; 12 | -webkit-box-align: center; 13 | -ms-flex-align: center; 14 | align-items: center; 15 | padding: 8px 16px; 16 | color: rgba(255, 255, 255, 0.87); 17 | img { 18 | width: 96px; 19 | } 20 | .active { 21 | box-shadow: 2px 4px 8px 2px mat-color($mat-blue, 800); 22 | } 23 | } 24 | .flex-spacer { 25 | -webkit-box-flex: 1; 26 | -ms-flex-positive: 1; 27 | flex-grow: 1; 28 | } 29 | #user-menu-btn { 30 | font-size: 22px; 31 | } 32 | } 33 | 34 | #user-box { 35 | padding: 1em; 36 | display: flex; 37 | align-items: center; 38 | border-bottom: solid 1px mat-color($mat-grey, 200); 39 | div { 40 | flex-direction: column; 41 | h3, 42 | h4 { 43 | margin: 0; 44 | } 45 | h3 { 46 | font-size: 16px; 47 | text-align: center; 48 | } 49 | h4 { 50 | font-weight: 100; 51 | font-size: 12px; 52 | } 53 | } 54 | img { 55 | margin-right: .8em; 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/client/app/shared/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { MaterialModule } from '../material.module' 2 | import { RouterTestingModule } from '@angular/router/testing' 3 | import { NavbarComponent } from './navbar.component' 4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 5 | import { INavbarService, NavbarService } from './navbar.service' 6 | import { By } from '@angular/platform-browser' 7 | import { Component } from '@angular/core' 8 | import { MatRipple } from '@angular/material' 9 | import '../../../operators' 10 | 11 | describe(NavbarComponent.name, () => { 12 | let fixture: ComponentFixture 13 | let navbarService: INavbarService 14 | 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [RouterTestingModule, MaterialModule], 18 | declarations: [NavbarComponent, TestComponent], 19 | providers: [NavbarService] 20 | }).compileComponents() 21 | })) 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(NavbarComponent) 25 | navbarService = TestBed.get(NavbarService) 26 | }) 27 | 28 | afterEach(() => { 29 | TestBed.resetTestingModule() 30 | }) 31 | 32 | it('should compile', async(() => { 33 | fixture.detectChanges() 34 | expect(fixture.nativeElement).toBeDefined() 35 | expect(fixture.nativeElement).toMatchSnapshot() 36 | })) 37 | 38 | it('should contain a list of links', async(() => { 39 | fixture.detectChanges() 40 | const buttonLinks = fixture.debugElement.queryAll(By.directive(MatRipple)) 41 | expect(buttonLinks).toBeDefined() 42 | 43 | navbarService.menu$.subscribe(items => { 44 | expect(buttonLinks.length).toEqual(items.length + 5) 45 | }) 46 | expect(fixture.nativeElement).toMatchSnapshot() 47 | })) 48 | }) 49 | 50 | @Component({ 51 | selector: 'test-component', 52 | template: '' 53 | }) 54 | class TestComponent {} 55 | -------------------------------------------------------------------------------- /src/client/app/shared/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, HostListener, Input, Output } from '@angular/core' 2 | import { NavbarService } from './navbar.service' 3 | 4 | export interface User { 5 | readonly photoURL: string 6 | readonly email: string 7 | readonly name: string 8 | } 9 | 10 | @Component({ 11 | selector: 'pm-navbar', 12 | templateUrl: './navbar.component.html', 13 | styleUrls: ['./navbar.component.scss'], 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class NavbarComponent { 17 | @HostListener('click', ['$event.target']) click() { 18 | this.clicked.next() 19 | } 20 | @Output() menuIconClick = new EventEmitter() 21 | @Output() clicked = new EventEmitter() 22 | @Input() user: User 23 | 24 | constructor(public navbarService: NavbarService) { } 25 | 26 | trackByFn(index: number, item: any) { 27 | return item.route 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/app/shared/navbar/navbar.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing' 2 | import { INavbarService, NavbarService } from './navbar.service' 3 | import { Observable } from 'rxjs/Observable' 4 | import '../../../operators' 5 | 6 | describe(NavbarService.name, () => { 7 | let service: INavbarService 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [NavbarService] 12 | }) 13 | })) 14 | 15 | beforeEach(() => { 16 | service = TestBed.get(NavbarService) 17 | }) 18 | 19 | afterEach(() => { 20 | TestBed.resetTestingModule() 21 | }) 22 | 23 | it('should construct', async(() => { 24 | expect(service).toBeDefined() 25 | })) 26 | 27 | it('should return an observable when called', async(() => { 28 | expect(service.menu$).toEqual(expect.any(Observable)) 29 | })) 30 | }) 31 | -------------------------------------------------------------------------------- /src/client/app/shared/navbar/navbar.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { Injectable } from '@angular/core' 3 | 4 | export interface INavbarService { 5 | readonly menu$: Observable> 6 | } 7 | 8 | @Injectable() 9 | export class NavbarService implements INavbarService { 10 | readonly menu$ = Observable.of([ 11 | { route: 'dashboard', name: 'Dashboard' }, 12 | { route: 'about', name: 'About' } 13 | ]) 14 | } 15 | -------------------------------------------------------------------------------- /src/client/app/shared/pipes/key-value.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ name: 'pmKeyVal' }) 4 | export class KeyValuePipe implements PipeTransform { 5 | public transform(value: { readonly [key: string]: any }): ReadonlyArray<{ readonly key: string, readonly value: any }> { 6 | return Object.keys(value || {}).map(key => ({ key, value: value[key] })) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/app/shared/pipes/keys.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ name: 'pmKeys' }) 4 | export class KeysPipe implements PipeTransform { 5 | public transform(value: { readonly [key: string]: any }): ReadonlyArray { 6 | return Object.keys(value || {}) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/app/shared/pipes/sanitize-html.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 3 | 4 | @Pipe({ 5 | name: 'pmSanitizeHtml' 6 | }) 7 | export class SanitizeHtmlPipe implements PipeTransform { 8 | constructor(private sanitizer: DomSanitizer) { } 9 | 10 | transform(v: string): SafeHtml { 11 | return this.sanitizer.bypassSecurityTrustHtml(v) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/app/shared/quill-editor/quill-editor.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/client/app/shared/quill-editor/quill-editor.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | height: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/client/app/shared/services/adblock.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http' 2 | import { Injectable } from '@angular/core' 3 | import { PlatformService } from './platform.service' 4 | import { Observable } from 'rxjs/Observable' 5 | 6 | export interface IAdblockService { 7 | adBlockerIsActive$: Observable 8 | } 9 | 10 | @Injectable() 11 | export class AdblockService implements IAdblockService { 12 | public adBlockerIsActive$ = this.platformService.isBrowser 13 | ? this.http.get('./ad-server.js') 14 | .switchMap(a => Observable.of(false)) 15 | .catch(a => Observable.of(true)) 16 | : Observable.of(false) 17 | 18 | constructor(private platformService: PlatformService, private http: HttpClient) { } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/app/shared/services/cookie.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { PlatformService } from './platform.service' 2 | import { CookieService, ICookieService } from './cookie.service' 3 | import { async, TestBed } from '@angular/core/testing' 4 | import { REQUEST } from '@nguniversal/express-engine/tokens' 5 | 6 | describe(CookieService.name, () => { 7 | let service: ICookieService 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [ 12 | CookieService, 13 | PlatformService, 14 | { provide: REQUEST, useValue: {} } 15 | ] 16 | }) 17 | })) 18 | 19 | beforeEach(() => { 20 | service = TestBed.get(CookieService) 21 | }) 22 | 23 | afterEach(() => { 24 | TestBed.resetTestingModule() 25 | }) 26 | 27 | it('should construct', async(() => { 28 | expect(service).toBeDefined() 29 | })) 30 | }) 31 | -------------------------------------------------------------------------------- /src/client/app/shared/services/cookie.service.ts: -------------------------------------------------------------------------------- 1 | import { REQUEST } from '@nguniversal/express-engine/tokens' 2 | import { PlatformService } from './platform.service' 3 | import { Inject, Injectable } from '@angular/core' 4 | import { Subject } from 'rxjs/Subject' 5 | import { Observable } from 'rxjs/Observable' 6 | import { CookieAttributes, getJSON, remove, set } from 'js-cookie' 7 | 8 | export interface ICookieService { 9 | readonly cookies$: Observable<{ readonly [key: string]: any }> 10 | getAll(): any 11 | get(name: string): any 12 | set(name: string, value: any, options?: CookieAttributes): void 13 | remove(name: string, options?: CookieAttributes): void 14 | } 15 | 16 | @Injectable() 17 | export class CookieService implements ICookieService { 18 | private readonly cookieSource = new Subject<{ readonly [key: string]: any }>() 19 | public readonly cookies$ = this.cookieSource.asObservable() 20 | 21 | constructor(private platformService: PlatformService, @Inject(REQUEST) private req: any) { } 22 | 23 | public set(name: string, value: any, options?: CookieAttributes): void { 24 | if (this.platformService.isBrowser) { 25 | set(name, value, options) 26 | this.updateSource() 27 | } 28 | } 29 | 30 | public remove(name: string, options?: CookieAttributes): void { 31 | if (this.platformService.isBrowser) { 32 | remove(name, options) 33 | this.updateSource() 34 | } 35 | } 36 | 37 | public get(name: string): any { 38 | if (this.platformService.isBrowser) { 39 | return getJSON(name) 40 | } else { 41 | try { 42 | return JSON.parse(this.req.cookies[name]) 43 | } catch (err) { 44 | return this.req ? this.req.cookies[name] : undefined 45 | } 46 | } 47 | } 48 | 49 | public getAll(): any { 50 | if (this.platformService.isBrowser) { 51 | return getJSON() 52 | } else { 53 | if (this.req) return this.req.cookies 54 | } 55 | } 56 | 57 | private updateSource() { 58 | this.cookieSource.next(this.getAll()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/client/app/shared/services/environment.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentService, IEnvironmentService } from './environment.service' 2 | import { async, TestBed } from '@angular/core/testing' 3 | import { ENV_CONFIG } from '../../app.config' 4 | 5 | describe(EnvironmentService.name, () => { 6 | let service: IEnvironmentService 7 | 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [ 11 | EnvironmentService, 12 | { provide: ENV_CONFIG, useValue: { someValue: 1 } } 13 | ] 14 | }) 15 | })) 16 | 17 | beforeEach(() => { 18 | service = TestBed.get(EnvironmentService) 19 | }) 20 | 21 | afterEach(() => { 22 | TestBed.resetTestingModule() 23 | }) 24 | 25 | it('should construct', async(() => { 26 | expect(service).toBeDefined() 27 | })) 28 | 29 | it('should return config', async(() => { 30 | expect(service.config).toEqual({ 31 | someValue: 1 32 | }) 33 | })) 34 | }) 35 | -------------------------------------------------------------------------------- /src/client/app/shared/services/environment.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core' 2 | import { ENV_CONFIG } from '../../app.config' 3 | import { EnvConfig } from '../../../../../tools/config/app.config' 4 | 5 | export interface IEnvironmentService { 6 | readonly config: EnvConfig 7 | } 8 | 9 | @Injectable() 10 | export class EnvironmentService implements IEnvironmentService { 11 | constructor(@Inject(ENV_CONFIG) private _config: any) { } 12 | get config(): EnvConfig { 13 | return this._config 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/app/shared/services/error-handler.service.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable, Injector } from '@angular/core' 2 | import { LocationStrategy, PathLocationStrategy } from '@angular/common' 3 | import { ILoggingService, LoggingService } from './logging.service' 4 | import { EnvironmentService, IEnvironmentService } from './environment.service' 5 | import * as StackTrace from 'stacktrace-js' 6 | 7 | @Injectable() 8 | export class GlobalErrorHandler implements ErrorHandler { 9 | constructor(private injector: Injector) { } 10 | 11 | handleError(error: any) { 12 | const log = this.injector.get(LoggingService) as ILoggingService 13 | const env = this.injector.get(EnvironmentService) as IEnvironmentService 14 | const location = this.injector.get(LocationStrategy) as LocationStrategy 15 | const message = error.message ? error.message : error.toString() 16 | const url = location instanceof PathLocationStrategy ? location.path() : '' 17 | 18 | // lets grab the last 20 stacks only 19 | StackTrace.fromError(error).then(stackframes => { 20 | const stack = stackframes 21 | .splice(0, 20) 22 | .map(sf => sf.toString()) 23 | .join('\n') 24 | 25 | log.error(message, { url, stack }, { config: env.config }) 26 | }).catch(err => log.error(message, { url, stack: err }, { config: env.config })) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/client/app/shared/services/guard-admin.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './auth.service' 2 | import { Injectable } from '@angular/core' 3 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router' 4 | import { Observable } from 'rxjs/Observable' 5 | import { ServerResponseService } from './server-response.service' 6 | import '../../../operators' 7 | 8 | @Injectable() 9 | export class AdminGuard implements CanActivate { 10 | 11 | constructor(private auth: AuthService, private router: Router, private srs: ServerResponseService) { } 12 | 13 | canActivate(ars: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 14 | return this.auth.isAdmin$.do(a => { 15 | if (!a) { 16 | this.srs.setUnauthorized() 17 | this.router.navigate(['unauthorized'], { queryParams: { redirect: state.url } }) 18 | } 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client/app/shared/services/guard-login.service.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './auth.service' 2 | import { Injectable } from '@angular/core' 3 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router' 4 | import { Observable } from 'rxjs/Observable' 5 | import '../../../operators' 6 | 7 | @Injectable() 8 | export class LoginGuard implements CanActivate { 9 | 10 | constructor(private auth: AuthService) { } 11 | 12 | canActivate(ars: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 13 | return this.auth.user$.map(a => a ? true : false) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/app/shared/services/http-config-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentService } from './environment.service' 2 | import { Observable } from 'rxjs/Observable' 3 | import { Injectable } from '@angular/core' 4 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http' 5 | 6 | @Injectable() 7 | export class HttpConfigInterceptor implements HttpInterceptor { 8 | 9 | constructor(private env: EnvironmentService) { } 10 | 11 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 12 | // handles absolute http requests 13 | if (req.url.includes('http')) return next.handle(req) 14 | 15 | if (!this.env.config.endpoints || !this.env.config.endpoints.api) throw new Error('missing endpoint configuration value') 16 | 17 | // handles relative urls on node server 18 | if (req.url.includes('./')) { 19 | if (!this.env.config.host) throw new Error('missing host configuration value') 20 | return next.handle(req.clone({ 21 | url: `${this.env.config.host}/${req.url.replace('./', '')}` 22 | })) 23 | } 24 | 25 | // handles entity requests to remote API 26 | return next.handle(req.clone({ 27 | url: `${this.env.config.endpoints.api}/${req.url}` 28 | })) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/client/app/shared/services/http-cookie-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { REQUEST } from '@nguniversal/express-engine/tokens' 2 | import { PlatformService } from './platform.service' 3 | import { Observable } from 'rxjs/Observable' 4 | import { Injectable, InjectionToken, Injector } from '@angular/core' 5 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http' 6 | import * as express from 'express' 7 | 8 | // tslint:disable-next-line:no-require-imports 9 | const URL = require('url-parse') 10 | 11 | export const COOKIE_HOST_WHITELIST = new InjectionToken('app.cookie.whitelist') 12 | 13 | @Injectable() 14 | export class HttpCookieInterceptor implements HttpInterceptor { 15 | 16 | constructor(private ps: PlatformService, private injector: Injector) { } 17 | 18 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 19 | const whitelistedDomains = this.injector.get(COOKIE_HOST_WHITELIST) as string[] 20 | const url = new URL(req.url) 21 | 22 | if (whitelistedDomains && !whitelistedDomains.some(a => a === url.hostname)) return next.handle(req) 23 | 24 | if (this.ps.isServer) { 25 | const serverRequest = this.injector.get(REQUEST) as express.Request 26 | 27 | const cookieString = Object.keys(serverRequest.cookies || {}).reduce((accumulator, cookieName) => { 28 | accumulator += `${cookieName}=${serverRequest.cookies[cookieName]};` 29 | return accumulator 30 | }, '') 31 | 32 | return next.handle(req.clone({ 33 | withCredentials: true, 34 | headers: req.headers.set('Cookie', cookieString) 35 | })) 36 | } 37 | return next.handle(req.clone({ 38 | withCredentials: true 39 | })) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/app/shared/services/injection.service.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/platform-browser' 2 | import { Inject, Injectable, Renderer2 } from '@angular/core' 3 | import { sha1 } from 'object-hash' 4 | 5 | export interface DOMInjectable { 6 | readonly inHead: boolean 7 | readonly element: string 8 | readonly value?: string 9 | readonly attributes?: { readonly [key: string]: string | boolean } 10 | } 11 | 12 | @Injectable() 13 | export class InjectionService { 14 | constructor(@Inject(DOCUMENT) private doc: any) { } 15 | 16 | createElement(renderer: Renderer2, injectable: DOMInjectable): T | undefined { 17 | if (!injectable || !injectable.element) return undefined 18 | const elm = renderer.createElement(injectable.element) as T 19 | const id = sha1(JSON.stringify(injectable)) 20 | 21 | renderer.setProperty(elm, 'id', id) 22 | if (injectable.value) renderer.setValue(elm, injectable.value) 23 | 24 | Object.keys(injectable.attributes || {}) 25 | .forEach(key => renderer.setAttribute(elm, key, (injectable.attributes || {})[key] as any)) 26 | 27 | return elm 28 | } 29 | 30 | getElementStringForm(renderer: Renderer2, injectable: DOMInjectable | undefined) { 31 | if (!injectable) return '' 32 | const elm = this.createElement(renderer, injectable) 33 | return ((elm && elm.outerHTML) || '').replace('><', `>${injectable.value}<`) 34 | } 35 | 36 | inject(renderer: Renderer2, injectable: DOMInjectable) { 37 | const elm = this.createElement(renderer, injectable) 38 | 39 | if (!elm || this.doc.getElementById(elm.id)) return 40 | 41 | injectable.inHead 42 | ? renderer.appendChild(this.doc.head, elm) 43 | : renderer.appendChild(this.doc.body, elm) 44 | } 45 | 46 | injectCollection(renderer: Renderer2, injectables: ReadonlyArray) { 47 | injectables.forEach(injectable => this.inject(renderer, injectable)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/client/app/shared/services/logging.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { PlatformService } from './platform.service' 2 | import { ILoggingService, LOGGER_CONFIG, LoggingService } from './logging.service' 3 | import { async, TestBed } from '@angular/core/testing' 4 | import { PLATFORM_ID } from '@angular/core' 5 | 6 | describe(LoggingService.name, () => { 7 | describe('browser', () => { 8 | let service: ILoggingService 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [ 12 | LoggingService, 13 | PlatformService, 14 | { provide: PLATFORM_ID, useValue: 'browser' }, 15 | { 16 | provide: LOGGER_CONFIG, 17 | useValue: { 18 | name: 'Angular Universal App', 19 | type: 'app' 20 | } 21 | } 22 | ] 23 | }) 24 | })) 25 | 26 | beforeEach(async(() => { 27 | service = TestBed.get(LoggingService) 28 | })) 29 | 30 | afterEach(async(() => { 31 | TestBed.resetTestingModule() 32 | })) 33 | 34 | it('should construct', async(() => { 35 | expect(service).toBeDefined() 36 | })) 37 | 38 | it('should call external logging library', async(() => { 39 | const traceSpy = jest.spyOn((service as any).logger, 'trace') 40 | const debugSpy = jest.spyOn((service as any).logger, 'debug') 41 | const infoSpy = jest.spyOn((service as any).logger, 'info') 42 | const warnSpy = jest.spyOn((service as any).logger, 'warn') 43 | const errorSpy = jest.spyOn((service as any).logger, 'error') 44 | service.trace('') 45 | service.debug('') 46 | service.info('') 47 | service.warn('') 48 | service.error('') 49 | expect(traceSpy).toHaveBeenCalled() 50 | expect(debugSpy).toHaveBeenCalled() 51 | expect(infoSpy).toHaveBeenCalled() 52 | expect(warnSpy).toHaveBeenCalled() 53 | expect(errorSpy).toHaveBeenCalled() 54 | })) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/client/app/shared/services/minifier.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | 3 | export interface IMinifierService { 4 | css(css: string): string 5 | } 6 | 7 | @Injectable() 8 | export class MinifierService implements IMinifierService { 9 | css(css: string): string { 10 | return css 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/app/shared/services/platform.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { IPlatformService, PlatformService } from './platform.service' 2 | import { async, TestBed } from '@angular/core/testing' 3 | import { PLATFORM_ID } from '@angular/core' 4 | 5 | describe(PlatformService.name, () => { 6 | let service: IPlatformService 7 | 8 | describe(`${PlatformService.name}.browser`, () => { 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [ 12 | PlatformService, 13 | { provide: PLATFORM_ID, useValue: 'browser' } 14 | ] 15 | }) 16 | })) 17 | 18 | beforeEach(async(() => { 19 | service = TestBed.get(PlatformService) 20 | })) 21 | 22 | afterEach(async(() => { 23 | TestBed.resetTestingModule() 24 | })) 25 | 26 | it('should construct', async(() => { 27 | expect(service).not.toBeNull() 28 | })) 29 | 30 | it('should be browser', async(() => { 31 | expect(service.isBrowser).toEqual(true) 32 | expect(service.isServer).toEqual(false) 33 | })) 34 | }) 35 | 36 | describe(`${PlatformService.name}.server`, () => { 37 | beforeEach(async(() => { 38 | TestBed.configureTestingModule({ 39 | providers: [ 40 | PlatformService, 41 | { provide: PLATFORM_ID, useValue: 'server' } 42 | ] 43 | }) 44 | })) 45 | 46 | beforeEach(async(() => { 47 | service = TestBed.get(PlatformService) 48 | })) 49 | 50 | afterEach(async(() => { 51 | TestBed.resetTestingModule() 52 | })) 53 | 54 | it('should be server', async(() => { 55 | expect(service.isServer).toEqual(true) 56 | expect(service.isBrowser).toEqual(false) 57 | })) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/client/app/shared/services/platform.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core' 2 | import { isPlatformBrowser, isPlatformServer } from '@angular/common' 3 | 4 | export interface IPlatformService { 5 | readonly isBrowser: boolean 6 | readonly isServer: boolean 7 | } 8 | 9 | @Injectable() 10 | export class PlatformService implements IPlatformService { 11 | constructor( @Inject(PLATFORM_ID) private platformId: any) { } 12 | 13 | public get isBrowser(): boolean { 14 | return isPlatformBrowser(this.platformId) 15 | } 16 | 17 | public get isServer(): boolean { 18 | return isPlatformServer(this.platformId) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/app/shared/services/setting.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing' 2 | import { IPlatformService, PlatformService } from './platform.service' 3 | import { SettingService } from './setting.service' 4 | import { async, TestBed } from '@angular/core/testing' 5 | import { FirebaseDatabaseService } from './firebase-database.service' 6 | import { of } from 'rxjs/observable/of' 7 | import '../../../operators' 8 | 9 | describe(SettingService.name, () => { 10 | let service: SettingService 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [HttpClientTestingModule], 15 | providers: [ 16 | SettingService, 17 | { provide: PlatformService, useValue: new MockPlatformService() }, 18 | { provide: FirebaseDatabaseService, useValue: new MockDb() } 19 | ] 20 | }) 21 | })) 22 | 23 | beforeEach(() => { 24 | service = TestBed.get(SettingService) 25 | }) 26 | 27 | it('should construct', async(() => { 28 | expect(service).toBeTruthy() 29 | })) 30 | }) 31 | 32 | class MockPlatformService implements IPlatformService { 33 | public isBrowser = true 34 | public isServer = false 35 | } 36 | 37 | class MockDb { 38 | get() { 39 | return of('test') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/app/shared/services/setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { Injectable } from '@angular/core' 3 | import { ISetting } from '../../../../server/api/services/setting.service' 4 | import { FirebaseDatabaseService } from './firebase-database.service' 5 | 6 | export interface ISettingService { 7 | settings$: Observable 8 | pluck(key: string): Observable 9 | } 10 | 11 | @Injectable() 12 | export class SettingService implements ISettingService { 13 | public initialSettings: ISetting 14 | public settings$ = this.db 15 | .get('site-settings') 16 | .map(settings => { 17 | return { 18 | injections: [], 19 | ...settings 20 | } as ISetting 21 | }) 22 | .shareReplay() 23 | 24 | public pluck(key: string) { 25 | return this.settings$.map(dict => key.split('.') 26 | .reduce((o, k) => (o || {})[k], dict as any)) 27 | } 28 | 29 | constructor(private db: FirebaseDatabaseService) { 30 | this.settings$.take(1).subscribe(set => this.initialSettings = set) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/app/shared/services/web-socket.service.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentService } from './environment.service' 2 | import { Subject } from 'rxjs/Subject' 3 | import { PlatformService } from './platform.service' 4 | import { Injectable } from '@angular/core' 5 | import { WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/observable/dom/WebSocketSubject' 6 | 7 | @Injectable() 8 | export class WebSocketService { 9 | private readonly source = this.ps.isBrowser && typeof window !== 'undefined' && (window as any).WebSocket && 10 | this.es.config.endpoints && this.es.config.endpoints.websocketServer 11 | ? new WebSocketSubject( 12 | { 13 | url: this.es.config.endpoints.websocketServer 14 | } as WebSocketSubjectConfig 15 | ) 16 | : new Subject() 17 | 18 | public readonly messageBus$ = this.source.asObservable() 19 | 20 | constructor(private ps: PlatformService, private es: EnvironmentService) { } 21 | 22 | send(obj: Object) { 23 | this.source.next(JSON.stringify(obj)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/app/shared/style-injection-form/style-injection-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 29 | 30 | 36 |
-------------------------------------------------------------------------------- /src/client/app/shared/style-injection-form/style-injection-form.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } -------------------------------------------------------------------------------- /src/client/app/shared/style-injection-form/style-injection-form.component.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms' 2 | import { ChangeDetectionStrategy, Component } from '@angular/core' 3 | // import { InjectionFormComponent } from '../injection-form/injection-form.component' 4 | 5 | @Component({ 6 | selector: 'pm-style-injection-form', 7 | templateUrl: './style-injection-form.component.html', 8 | styleUrls: ['./style-injection-form.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class StyleInjectionFormComponent { 12 | isInline = new FormControl(true) 13 | 14 | constructor() { 15 | // super() 16 | // this.form.addControl('linkHref', new FormControl('', [])) 17 | // this.form.controls['element'].setValue('link') 18 | // this.isInline.valueChanges.subscribe(inline => { 19 | // if (inline) { 20 | // this.form.controls['element'].setValue('style') 21 | // this.form.controls['linkHref'].setValidators([]) 22 | // this.form.controls['linkHref'].setValue(undefined) 23 | // this.form.controls['attributes'].setValue(undefined) 24 | // } else { 25 | // this.form.controls['linkHref'].setValidators([Validators.required]) 26 | // this.form.controls['element'].setValue('link') 27 | // this.form.controls['attributes'].setValue({ 28 | // ...this.form.controls['attributes'].value, 29 | // rel: 'stylesheet', 30 | // type: 'text/css', 31 | // href: '' 32 | // }) 33 | // } 34 | // }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/client/app/shared/web-app-installer/web-app-installer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
To install tap 5 |
and choose 6 |
7 |
8 |
-------------------------------------------------------------------------------- /src/client/app/shared/web-app-installer/web-app-installer.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-web-app-installer', 5 | templateUrl: './web-app-installer.component.html', 6 | styleUrls: ['./web-app-installer.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class WebAppInstallerComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/shared/web-app-installer/web-app-installer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { WebAppInstallerService } from './web-app-installer.service' 3 | import { WebAppInstallerComponent } from './web-app-installer.component' 4 | 5 | @NgModule({ 6 | declarations: [WebAppInstallerComponent], 7 | exports: [WebAppInstallerComponent], 8 | providers: [WebAppInstallerService] 9 | }) 10 | export class WebAppInstallerlModule { } 11 | -------------------------------------------------------------------------------- /src/client/app/shared/web-app-installer/web-app-installer.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core' 2 | import { isPlatformBrowser } from '@angular/common' 3 | 4 | @Injectable() 5 | export class WebAppInstallerService { 6 | get isIosWebApp() { 7 | return isPlatformBrowser(this.platformId) && 8 | 'standalone' in window.navigator && 9 | (window.navigator as any).standalone !== true 10 | } 11 | 12 | constructor(@Inject(PLATFORM_ID) private platformId: any) { } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/app/signup/__snapshots__/signup.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SignupComponent should match snapshot 1`] = ` 4 | 5 | Your 6 | signup 7 | form goes here! 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /src/client/app/signup/signup-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { SignupComponent } from './signup.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: SignupComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.signup.title', 16 | description: 'i18n.signup.description' 17 | } 18 | } 19 | } 20 | ]) 21 | ], 22 | exports: [RouterModule] 23 | }) 24 | export class SignupRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.component.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { baseUrl, browser } from '../../../../tools/test/jest.e2e-setup' 2 | 3 | describe('Signup Page', () => { 4 | it('should have title', async () => { 5 | expect.assertions(1) 6 | 7 | const page = browser.goto(`${baseUrl}/signup`) 8 | 9 | const text = await page.evaluate(() => document.title) 10 | 11 | expect(text).toContain('Signup - Fusebox Angular Universal Starter') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.component.html: -------------------------------------------------------------------------------- 1 | Your signup form goes here! -------------------------------------------------------------------------------- /src/client/app/signup/signup.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/signup/signup.component.scss -------------------------------------------------------------------------------- /src/client/app/signup/signup.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { SignupComponent } from './signup.component' 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 3 | import { Component } from '@angular/core' 4 | import { SignupModule } from './signup.module' 5 | import { AppTestingModule } from '../../../testing/app-testing.module' 6 | 7 | describe(SignupComponent.name, () => { 8 | let fixture: ComponentFixture 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [AppTestingModule.forRoot(), SignupModule], 13 | declarations: [TestComponent] 14 | }).compileComponents() 15 | })) 16 | 17 | beforeEach(async(() => { 18 | fixture = TestBed.createComponent(SignupComponent) 19 | })) 20 | 21 | afterEach(async(() => { 22 | TestBed.resetTestingModule() 23 | })) 24 | 25 | it('should match snapshot', async(() => { 26 | fixture.detectChanges() 27 | expect(fixture).toMatchSnapshot() 28 | })) 29 | 30 | it('should compile', async(() => { 31 | fixture.detectChanges() 32 | expect(fixture.nativeElement).toBeDefined() 33 | })) 34 | }) 35 | 36 | @Component({ 37 | selector: 'test-component', 38 | template: '' 39 | }) 40 | class TestComponent {} 41 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-signup', 5 | templateUrl: './signup.component.html', 6 | styleUrls: ['./signup.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class SignupComponent { } 10 | -------------------------------------------------------------------------------- /src/client/app/signup/signup.module.ts: -------------------------------------------------------------------------------- 1 | import { SignupComponent } from './signup.component' 2 | import { SignupRoutingModule } from './signup-routing.module' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [SignupRoutingModule, SharedModule], 8 | declarations: [SignupComponent], 9 | exports: [SignupComponent] 10 | }) 11 | export class SignupModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/unauthorized/__snapshots__/unauthorized.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UnauthorizedComponent should compile 1`] = ` 4 | 5 |
9 |

10 | Unauthorized 11 |

12 | 13 | 14 | 15 | 16 |
17 |
18 | `; 19 | -------------------------------------------------------------------------------- /src/client/app/unauthorized/unauthorized-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedComponent } from './unauthorized.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: UnauthorizedComponent, 12 | canActivate: [MetaGuard], 13 | data: { 14 | meta: { 15 | title: 'i18n.unauthorized.title', 16 | description: 'i18n.unauthorized.description' 17 | } 18 | } 19 | } 20 | ]) 21 | ], 22 | exports: [RouterModule] 23 | }) 24 | export class UnauthorizedRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/client/app/unauthorized/unauthorized.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Unauthorized

3 | 4 |
-------------------------------------------------------------------------------- /src/client/app/unauthorized/unauthorized.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/app/unauthorized/unauthorized.component.scss -------------------------------------------------------------------------------- /src/client/app/unauthorized/unauthorized.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './../shared/services/auth.service' 2 | import { AngularFireAuthModule } from 'angularfire2/auth' 3 | import { UnauthorizedComponent } from './unauthorized.component' 4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 5 | import { Component } from '@angular/core' 6 | import { UnauthorizedModule } from './unauthorized.module' 7 | import { AngularFireModule } from 'angularfire2' 8 | import { AppTestingModule } from '../../../testing/app-testing.module' 9 | 10 | @Component({ 11 | selector: 'test-component', 12 | template: '' 13 | }) 14 | class TestComponent { } 15 | 16 | describe(UnauthorizedComponent.name, () => { 17 | let fixture: ComponentFixture 18 | 19 | beforeEach(async(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [AppTestingModule.forRoot(), UnauthorizedModule, AngularFireAuthModule, AngularFireModule.initializeApp({ 22 | 'apiKey': '1', 23 | 'authDomain': 'app.firebaseapp.com', 24 | 'databaseURL': 'https://app.firebaseio.com', 25 | 'projectId': 'firebase-app', 26 | 'messagingSenderId': '1' 27 | })], 28 | declarations: [TestComponent], 29 | providers: [ 30 | { provide: AuthService, useValue: {} } 31 | ] 32 | }).compileComponents() 33 | })) 34 | 35 | beforeEach(async(() => { 36 | fixture = TestBed.createComponent(UnauthorizedComponent) 37 | })) 38 | 39 | afterEach(async(() => { 40 | TestBed.resetTestingModule() 41 | })) 42 | 43 | it('should compile', async(() => { 44 | expect(fixture.nativeElement).toBeDefined() 45 | expect(fixture).toMatchSnapshot() 46 | })) 47 | }) 48 | -------------------------------------------------------------------------------- /src/client/app/unauthorized/unauthorized.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-unauthorized', 5 | templateUrl: './unauthorized.component.html', 6 | styleUrls: ['./unauthorized.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class UnauthorizedComponent { 10 | } 11 | -------------------------------------------------------------------------------- /src/client/app/unauthorized/unauthorized.module.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedRoutingModule } from './unauthorized-routing.module' 2 | import { UnauthorizedComponent } from './unauthorized.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [UnauthorizedRoutingModule, SharedModule], 8 | declarations: [UnauthorizedComponent], 9 | exports: [UnauthorizedComponent] 10 | }) 11 | export class UnauthorizedModule { } 12 | -------------------------------------------------------------------------------- /src/client/app/users/__snapshots__/users.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UsersComponent should compile 1`] = ` 4 | 5 | 6 |
11 | 16 | 17 | 18 |
19 |
20 |
21 | `; 22 | -------------------------------------------------------------------------------- /src/client/app/users/users-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { UsersComponent } from './users.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | import { MetaGuard } from '@ngx-meta/core' 5 | import { AdminGuard } from '../shared/services/guard-admin.service' 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: '', 12 | component: UsersComponent, 13 | canActivate: [MetaGuard, AdminGuard], 14 | data: { 15 | meta: { 16 | title: 'i18n.users.title', 17 | description: 'i18n.users.description' 18 | }, 19 | response: { 20 | cache: { 21 | directive: 'private' 22 | } 23 | } 24 | } 25 | } 26 | ]) 27 | ], 28 | exports: [RouterModule] 29 | }) 30 | export class UsersRoutingModule { } 31 | -------------------------------------------------------------------------------- /src/client/app/users/users.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

{{ user.displayName }}

6 |

{{ user.email }}

7 |
8 |
9 | 10 |
-------------------------------------------------------------------------------- /src/client/app/users/users.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | mat-nav-list { 3 | overflow-y: auto; 4 | } 5 | } -------------------------------------------------------------------------------- /src/client/app/users/users.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { UsersComponent } from './users.component' 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 3 | import { Component } from '@angular/core' 4 | import { UsersModule } from './users.module' 5 | import { AppTestingModule } from '../../../testing/app-testing.module' 6 | 7 | @Component({ 8 | selector: 'test-component', 9 | template: '' 10 | }) 11 | class TestComponent { } 12 | 13 | describe(UsersComponent.name, () => { 14 | let fixture: ComponentFixture 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [AppTestingModule.forRoot(), UsersModule], 19 | declarations: [TestComponent] 20 | }).compileComponents() 21 | })) 22 | 23 | beforeEach(async(() => { 24 | fixture = TestBed.createComponent(TestComponent) 25 | })) 26 | 27 | afterEach(async(() => { 28 | TestBed.resetTestingModule() 29 | })) 30 | 31 | it('should compile', async(() => { 32 | expect(fixture.nativeElement).toBeDefined() 33 | expect(fixture).toMatchSnapshot() 34 | })) 35 | }) 36 | -------------------------------------------------------------------------------- /src/client/app/users/users.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { ChangeDetectionStrategy, Component } from '@angular/core' 3 | import { FirebaseDatabaseService } from '../shared/services/firebase-database.service' 4 | import { SettingService } from '../shared/services/setting.service' 5 | // import { PageEvent } from '@angular/material' 6 | 7 | @Component({ 8 | selector: 'pm-users', 9 | templateUrl: './users.component.html', 10 | styleUrls: ['./users.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class UsersComponent { 14 | 15 | public usersFromDb$ = this.db 16 | .getListKeyed('users', ref => ref.limitToFirst(5000)) // TODO: pagination 17 | 18 | public users$ = Observable.combineLatest(this.usersFromDb$, this.ss.settings$, 19 | (userInfo, settings) => { 20 | return userInfo.map((user: any) => { 21 | return { 22 | ...user, 23 | photo: user.photo || settings.assets.userAvatarImage 24 | } 25 | }) 26 | }) 27 | 28 | constructor(private db: FirebaseDatabaseService, private ss: SettingService) { } 29 | 30 | // pageEvent(pageEvent: PageEvent) { 31 | // console.log(pageEvent) 32 | // } 33 | 34 | trackByUsers(index: number, item: any) { 35 | return item.id 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/client/app/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { UsersRoutingModule } from './users-routing.module' 2 | import { UsersComponent } from './users.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | 6 | @NgModule({ 7 | imports: [UsersRoutingModule, SharedModule], 8 | declarations: [UsersComponent], 9 | exports: [UsersComponent] 10 | }) 11 | export class UsersModule { } 12 | -------------------------------------------------------------------------------- /src/client/assets/angular-white-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/client/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/assets/favicon.png -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/client/main-prod.ts: -------------------------------------------------------------------------------- 1 | import './polyfills' 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 4 | import { enableProdMode } from '@angular/core' 5 | import { AppBrowserModule } from './app/app.browser.module' 6 | 7 | enableProdMode() 8 | 9 | platformBrowserDynamic().bootstrapModule(AppBrowserModule) 10 | -------------------------------------------------------------------------------- /src/client/main.aot-prod.ts: -------------------------------------------------------------------------------- 1 | import './polyfills' 2 | 3 | import { enableProdMode } from '@angular/core' 4 | import { platformBrowser } from '@angular/platform-browser' 5 | import { AppBrowserModuleNgFactory } from './.aot/src/client/app/app.browser.module.ngfactory' 6 | 7 | enableProdMode() 8 | 9 | platformBrowser().bootstrapModuleFactory(AppBrowserModuleNgFactory) 10 | -------------------------------------------------------------------------------- /src/client/main.aot.ts: -------------------------------------------------------------------------------- 1 | import './polyfills' 2 | 3 | import { platformBrowser } from '@angular/platform-browser' 4 | import { AppBrowserModuleNgFactory } from './.aot/src/client/app/app.browser.module.ngfactory' 5 | 6 | platformBrowser().bootstrapModuleFactory(AppBrowserModuleNgFactory) 7 | -------------------------------------------------------------------------------- /src/client/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills' 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 4 | import { AppBrowserModule } from './app/app.browser.module' 5 | 6 | platformBrowserDynamic().bootstrapModule(AppBrowserModule) 7 | -------------------------------------------------------------------------------- /src/client/ngsw.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/", 3 | "appData": { 4 | "test": true 5 | }, 6 | "assetGroups": [ 7 | { 8 | "name": "appshell", 9 | "resources": { 10 | "files": [ 11 | "/**/*!(index).html", 12 | "/**/*.js", 13 | "/**/*.css", 14 | "/assets/**/*", 15 | "!/ngsw-worker.js", 16 | "!/server.js", 17 | "!/keystore.json", 18 | "!/web/ping.html" 19 | ], 20 | "versionedFiles": [], 21 | "urls": [ 22 | "/absolute/**", 23 | "relative/*.txt" 24 | ] 25 | } 26 | } 27 | ], 28 | "dataGroups": [ 29 | { 30 | "name": "api", 31 | "urls": [ 32 | "/api/**" 33 | ], 34 | "cacheConfig": { 35 | "maxSize": 100, 36 | "maxAge": "1d", 37 | "timeout": "1m", 38 | "strategy": "freshness" 39 | } 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /src/client/operators.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/observable/of' 2 | import 'rxjs/add/observable/throw' 3 | import 'rxjs/add/observable/fromPromise' 4 | import 'rxjs/add/observable/combineLatest' 5 | import 'rxjs/add/operator/startWith' 6 | import 'rxjs/add/operator/map' 7 | import 'rxjs/add/operator/catch' 8 | import 'rxjs/add/operator/switchMap' 9 | import 'rxjs/add/operator/do' 10 | import 'rxjs/add/operator/pluck' 11 | import 'rxjs/add/operator/filter' 12 | import 'rxjs/add/operator/share' 13 | import 'rxjs/add/operator/shareReplay' 14 | import 'rxjs/add/operator/toPromise' 15 | import 'rxjs/add/operator/take' 16 | import 'rxjs/add/operator/throttleTime' 17 | import 'rxjs/add/operator/distinctUntilChanged' 18 | import 'rxjs/add/operator/last' 19 | import 'rxjs/add/operator/takeLast' 20 | import 'rxjs/add/operator/debounceTime' 21 | import 'rxjs/add/operator/finally' 22 | import 'rxjs/add/operator/auditTime' 23 | import 'rxjs/add/operator/takeUntil' 24 | import 'rxjs/add/operator/skip' 25 | -------------------------------------------------------------------------------- /src/client/polyfills.ts: -------------------------------------------------------------------------------- 1 | // Fusebox 2 | if (process.env.NODE_ENV !== 'development') { 3 | require = {} as any 4 | } 5 | 6 | // Required for Angular 7 | import 'core-js/es7/reflect' 8 | import 'zone.js/dist/zone' 9 | 10 | // IE9, IE10 and IE11 require all of the following polyfills. 11 | // import 'core-js/es6/symbol' 12 | // import 'core-js/es6/object' 13 | // import 'core-js/es6/function' 14 | // import 'core-js/es6/parse-int' 15 | // import 'core-js/es6/parse-float' 16 | // import 'core-js/es6/number' 17 | // import 'core-js/es6/math' 18 | // import 'core-js/es6/string' 19 | // import 'core-js/es6/date' 20 | // import 'core-js/es6/array' 21 | // import 'core-js/es6/regexp' 22 | // import 'core-js/es6/map' 23 | // import 'core-js/es6/weak-map' 24 | // import 'core-js/es6/set' 25 | 26 | // IE10 and IE11 requires the following to support `@angular/animation`. 27 | // import 'web-animations-js' 28 | 29 | // rxjs 30 | import './operators' 31 | -------------------------------------------------------------------------------- /src/client/styles/main.scss: -------------------------------------------------------------------------------- 1 | $fa-font-path: '/assets/fonts/font-awesome'; 2 | @import '../../../node_modules/font-awesome/scss/font-awesome'; 3 | @import './node_modules/@angular/material/_theming.scss'; 4 | @import './node_modules/quill/dist/quill.snow'; 5 | @include mat-core(); 6 | $primary: mat-palette($mat-blue, 700); 7 | $accent: mat-palette($mat-pink); 8 | $theme: mat-light-theme($primary, $accent); 9 | @include angular-material-theme($theme); 10 | .dark-theme { 11 | $dark-p: mat-palette($mat-pink, 700); 12 | $dark-a: mat-palette($mat-blue-grey); 13 | $dark-t: mat-dark-theme($dark-p, $dark-a); 14 | @include angular-material-theme($dark-t); 15 | } 16 | 17 | html { 18 | font-family: Roboto, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; 19 | } 20 | 21 | html, 22 | body { 23 | margin: 0; 24 | display: flex; 25 | flex: 1 0; 26 | flex-direction: column; 27 | height: 100%; 28 | } 29 | 30 | .avatar { 31 | height: 40px; 32 | width: 40px; 33 | border-radius: 50%; 34 | display: inline-block; 35 | } 36 | 37 | .card-float-container { 38 | display: flex; 39 | justify-content: center; 40 | margin: 1em; 41 | @media only screen and (max-width: 959px) { 42 | display: block; 43 | margin: 0; 44 | } 45 | } 46 | 47 | // TODO remove in future 48 | .mat-card-header-text { 49 | width: 100%; 50 | } 51 | 52 | // TODO remove in future 53 | .mat-form-field.mat-form-field { 54 | width: auto; 55 | } 56 | 57 | .mat-raised-button .mat-button-wrapper>* { 58 | vertical-align: inherit !important; 59 | } 60 | 61 | .mat-menu-content { 62 | padding: 0 !important; 63 | } 64 | 65 | .vert-flex-fill { 66 | display: flex; 67 | flex-direction: column; 68 | flex: 1; 69 | height: 100%; 70 | } 71 | 72 | .ql-toolbar.ql-snow { 73 | border: none !important; 74 | } 75 | 76 | .ql-container.ql-snow { 77 | border: none; 78 | } -------------------------------------------------------------------------------- /src/client/styles/material2-app-themes.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusebox-angular-universal-starter/ab8c89e1f9c525f0cd7a7d65869ba782824b3943/src/client/styles/material2-app-themes.scss -------------------------------------------------------------------------------- /src/server/api/api.spec.ts: -------------------------------------------------------------------------------- 1 | import { testApi } from './test-helper' 2 | 3 | describe('API Server', () => { 4 | it('should have swagger.json route', () => { 5 | return testApi.get('/api-docs.json') 6 | .expect(200) 7 | .expect('Content-Type', /json/) 8 | }) 9 | 10 | it('should have swagger docs route', () => { 11 | return testApi.get('/api-docs/') 12 | .expect(200) 13 | .expect('Content-Type', /html/) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/server/api/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import { SettingsController } from './settings.controller' 2 | 3 | export const controllers = [ 4 | SettingsController 5 | ] 6 | -------------------------------------------------------------------------------- /src/server/api/controllers/settings.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { testApi } from '../test-helper' 2 | import { SettingsController } from './settings.controller' 3 | 4 | describe(SettingsController.name, () => { 5 | it('should get settings object', () => { 6 | return testApi.get('/api/settings') 7 | .expect(200) 8 | .expect('Content-Type', 'application/json; charset=utf-8') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/server/api/controllers/settings.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, JsonController } from 'routing-controllers' 2 | import { SettingService } from '../services' 3 | 4 | @JsonController() 5 | export class SettingsController { 6 | constructor(private settingService: SettingService) { } 7 | 8 | /** 9 | * @swagger 10 | * tags: 11 | * - name: settings 12 | * description: operations on the application's settings 13 | */ 14 | 15 | /** 16 | * @swagger 17 | * /api/settings: 18 | * get: 19 | * tags: [settings] 20 | * description: get the site settings 21 | * produces: 22 | * - application/json 23 | * responses: 24 | * 200: 25 | * description: success 26 | */ 27 | @Get('/settings') 28 | get() { 29 | return this.settingService.settings$.take(1).toPromise() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/api/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-require-imports 2 | 3 | import { useContainer, useExpressServer as configApi } from 'routing-controllers' 4 | import { Container } from 'typedi' 5 | import { controllers } from './controllers' 6 | import { middlewares } from './middlewares' 7 | import * as express from 'express' 8 | import * as bodyParser from 'body-parser' 9 | const swaggerJSDoc = require('swagger-jsdoc') 10 | const swaggerUi = require('swagger-ui-express') 11 | 12 | useContainer(Container) 13 | 14 | export const useApi = (app: express.Application) => { 15 | const swaggerSpec = swaggerJSDoc({ 16 | swaggerDefinition: { 17 | info: { 18 | title: 'fusebox-angular-universal-starter', 19 | description: '', 20 | termsOfService: '', 21 | contact: { 22 | name: 'Patrick Michalina', 23 | url: 'https://github.com/patrickmichalina/fusebox-angular-universal-starter/issues', 24 | email: 'patrickmichalina@mac.com' 25 | }, 26 | license: { 27 | name: 'MIT', 28 | url: 'https://github.com/patrickmichalina/fusebox-angular-universal-starter/blob/master/LICENSE' 29 | }, 30 | version: '1.0.0' 31 | } 32 | }, 33 | apis: ['./src/server/api/controllers/**/*.ts'] 34 | }) 35 | 36 | app.use('/api/**', bodyParser.json()) 37 | app.use('/api/**', bodyParser.urlencoded({ extended: false })) 38 | app.get('/api-docs.json', (req, res) => { 39 | res.setHeader('Content-Type', 'application/json') 40 | res.send(swaggerSpec) 41 | }) 42 | 43 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)) 44 | 45 | configApi(app, { 46 | cors: true, 47 | validation: true, 48 | routePrefix: '/api', 49 | controllers, 50 | middlewares, 51 | defaultErrorHandler: false, 52 | defaults: { 53 | nullResultCode: 404, 54 | undefinedResultCode: 204 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/server/api/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import { ZoneErrorHandler } from './zone-error-handler' 2 | 3 | export const middlewares = [ 4 | ZoneErrorHandler 5 | ] 6 | -------------------------------------------------------------------------------- /src/server/api/middlewares/zone-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { ExpressErrorMiddlewareInterface, Middleware } from 'routing-controllers' 2 | import * as express from 'express' 3 | 4 | @Middleware({ type: 'after' }) 5 | export class ZoneErrorHandler implements ExpressErrorMiddlewareInterface { 6 | // Simply supresses zonejs error 7 | error(error: any, request: express.Request, response: express.Response) { 8 | const err = { 9 | httpCode: error.httpCode, 10 | name: error.name, 11 | message: error.message, 12 | errors: error.errors 13 | } 14 | response.status(error.httpCode).send(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server/api/repositories/index.ts: -------------------------------------------------------------------------------- 1 | import { SettingRepository } from './setting.repository' 2 | 3 | export { 4 | SettingRepository 5 | } 6 | -------------------------------------------------------------------------------- /src/server/api/services/index.ts: -------------------------------------------------------------------------------- 1 | import { SettingService } from './setting.service' 2 | 3 | export { 4 | SettingService 5 | } 6 | -------------------------------------------------------------------------------- /src/server/api/services/setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | import { Service } from 'typedi' 3 | import { ISetting, SettingRepository } from '../repositories/setting.repository' 4 | export { ISetting } 5 | 6 | export interface ISettingService { 7 | readonly settings$: Observable 8 | // getTranslation(lang: string): Observable 9 | // getDictionary(): Observable 10 | // getLanguageDictionary(lang: string): Observable 11 | } 12 | 13 | @Service() 14 | export class SettingService implements ISettingService { 15 | readonly settings$ = this.repo.getDictionary() 16 | 17 | constructor(private repo: SettingRepository) { } 18 | 19 | // getDictionary(): Observable { 20 | // const dictionary = this.repo.getDictionary() 21 | // return Observable.of(dictionary) 22 | // } 23 | 24 | // getLanguageDictionary(lang: string): Observable { 25 | // throw new Error('Method not implemented.') 26 | // } 27 | 28 | // getTranslation(key: string, lang = 'EN') { 29 | // const dictionary = this.repo.getDictionary() 30 | // if (!dictionary) throw new Error('') 31 | 32 | // const languageContext = dictionary[lang] 33 | // const value = key.split('.').reduce((o, k) => (o || {})[k], languageContext) 34 | 35 | // return Observable.of(value) 36 | // } 37 | } 38 | -------------------------------------------------------------------------------- /src/server/api/test-helper.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-require-imports 2 | // tslint:disable-next-line:import-blacklist 3 | import 'rxjs' 4 | import * as supertest from 'supertest' 5 | import { useApi } from './index' 6 | const express = require('express') 7 | const app = express() 8 | useApi(app) 9 | 10 | export const testApi = supertest.agent(app) 11 | -------------------------------------------------------------------------------- /src/server/server.config.ts: -------------------------------------------------------------------------------- 1 | import { config as dotenv } from 'dotenv' 2 | import { writeFileSync } from 'fs' 3 | import { EnvConfig } from '../../tools/config/app.config' 4 | declare var __process_env__: any 5 | 6 | export function fuseBoxConfigFactory() { 7 | return __process_env__ as EnvConfig 8 | } 9 | 10 | export interface ServerEnvironmentConfig { 11 | FB_SERVICE_ACCOUNT_PRIVATE_KEY_ID: string 12 | FB_SERVICE_ACCOUNT_PRIVATE_KEY: string 13 | } 14 | 15 | // this comes from fusebox 16 | export const ANGULAR_APP_CONFIG = fuseBoxConfigFactory() 17 | 18 | const env = dotenv() 19 | if (env.error && ANGULAR_APP_CONFIG.env === 'dev') { 20 | console.error('.env development file created!\nYOU MUST ADD YOUR CONFIGURATION VALUES TO IT') 21 | writeFileSync('.env', 22 | `FB_SERVICE_ACCOUNT_PRIVATE_KEY_ID= 23 | FB_SERVICE_ACCOUNT_PRIVATE_KEY=`) 24 | } 25 | 26 | const errors: string[] = [] 27 | 28 | if (!process.env.FB_SERVICE_ACCOUNT_PRIVATE_KEY_ID) errors.push('Missing FB_SERVICE_ACCOUNT_PRIVATE_KEY_ID') 29 | if (!process.env.FB_SERVICE_ACCOUNT_PRIVATE_KEY) errors.push('Missing FB_SERVICE_ACCOUNT_PRIVATE_KEY') 30 | 31 | if (errors.length > 0) { 32 | console.error('Invalid Configuration') 33 | console.error(errors.join('\n')) 34 | } 35 | 36 | export const SERVER_CONFIG: ServerEnvironmentConfig = process.env as any 37 | 38 | // tslint:disable-next-line:no-require-imports 39 | const base = require('./service-account.json') 40 | 41 | export const FB_SERVICE_ACCOUNT_CONFIG = { 42 | ...base, 43 | private_key_id: SERVER_CONFIG.FB_SERVICE_ACCOUNT_PRIVATE_KEY_ID, 44 | private_key: SERVER_CONFIG.FB_SERVICE_ACCOUNT_PRIVATE_KEY && SERVER_CONFIG.FB_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/g, '\n'), 45 | project_id: ANGULAR_APP_CONFIG.firebase.config.projectId 46 | } 47 | -------------------------------------------------------------------------------- /src/server/server.sitemap.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-require-imports 2 | 3 | import { writeFile } from 'fs' 4 | const sitemapGenerator = require('sitemap-generator') 5 | 6 | export const sitemap = (host: string) => new Promise((resolve, reject) => { 7 | const generator = new sitemapGenerator(host) 8 | 9 | // Avoid infinite loop during initial creation 10 | generator.crawler.addFetchCondition((parsedUrl: any) => { 11 | return !parsedUrl.url.includes('sitemap.xml') 12 | }) 13 | 14 | // TODO: this isn't working! 15 | // generator.crawler.on('fetchconditionerror', (queueItem: any) => { 16 | // add back into sitemap stack 17 | // }); 18 | 19 | generator.on('done', (sitemaps: string[]) => { 20 | if (sitemaps && sitemaps[0]) { 21 | writeFile('dist/sitemap.xml', sitemaps[0], () => { 22 | console.log('Generated sitemap.xml') 23 | return resolve(sitemaps[0]) 24 | }) 25 | } else { 26 | return reject('Failed to generate sitemap.xml') 27 | } 28 | }) 29 | 30 | generator.on('clienterror', (err: any) => { 31 | return reject(err) 32 | }) 33 | 34 | console.log(`starting sitemap crawler on ${host}`) 35 | generator.start() 36 | }) 37 | -------------------------------------------------------------------------------- /src/server/server.web-socket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http' 2 | import { Server as wsServer } from 'ws' 3 | 4 | export const useWebSockets = (server: Server): wsServer => { 5 | const wss = new wsServer({ server }) 6 | 7 | // function heartbeat() { 8 | // // tslint:disable:no-invalid-this 9 | // console.log(heartbeat, this.isAlive) 10 | // this.isAlive = true 11 | // console.log(heartbeat, this.isAlive) 12 | // } 13 | 14 | wss.on('connection', (ws, req) => { 15 | // ws.isAlive = true 16 | // ws.on('pong', heartbeat) 17 | ws.on('message', message => { 18 | if (message && typeof message === 'string') { 19 | try { 20 | ws.send(JSON.stringify({ ...JSON.parse(message), server: 'Relayed from' })) 21 | } catch (err) { 22 | ws.send('an error occurred in the ws channel') 23 | } 24 | } 25 | }) 26 | }) 27 | 28 | // setInterval(() => { 29 | // wss.clients.forEach(ws => { 30 | // // if (ws.isAlive === false) return ws.terminate() 31 | // ws.isAlive = false 32 | // ws.ping('', false, true) 33 | // }) 34 | // }, 1000) 35 | return wss 36 | } 37 | -------------------------------------------------------------------------------- /src/server/service-account.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "fuse-angular-universal-s-67402", 4 | "private_key_id": "STORE_IN_ENVIRONMENT_VARIABLE_FOR_SECURITY", 5 | "private_key": "STORE_IN_ENVIRONMENT_VARIABLE_FOR_SECURITY", 6 | "client_email": "firebase-adminsdk-4wy6u@fuse-angular-universal-s-67402.iam.gserviceaccount.com", 7 | "client_id": "116281772478555427333", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-4wy6u%40fuse-angular-universal-s-67402.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /src/testing/mock-cookie.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { ICookieService } from '../client/app/shared/services/cookie.service' 3 | import { BehaviorSubject } from 'rxjs/BehaviorSubject' 4 | 5 | @Injectable() 6 | export class MockCookieService implements ICookieService { 7 | private cookieSource = new BehaviorSubject<{ [key: string]: any }>(this.getAll()) 8 | public cookies$ = this.cookieSource.asObservable() 9 | public mockCookieStore: any = { } 10 | 11 | get(name: string): any { 12 | return this.mockCookieStore[name] 13 | } 14 | 15 | getAll() { 16 | return this.mockCookieStore || {} 17 | } 18 | 19 | set(name: string, value: any, options?: Cookies.CookieAttributes | undefined): void { 20 | this.mockCookieStore[name] = value 21 | this.cookieSource.next(this.getAll()) 22 | } 23 | 24 | remove(name: string, options?: Cookies.CookieAttributes | undefined): void { 25 | const { [name]: omit, ...newObject } = this.mockCookieStore 26 | this.mockCookieStore = newObject 27 | this.cookieSource.next(this.getAll()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/testing/mock-environment.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { IEnvironmentService } from '../client/app/shared/services/environment.service' 3 | 4 | @Injectable() 5 | export class MockEnvironmentService implements IEnvironmentService { 6 | config: any = {} 7 | } 8 | -------------------------------------------------------------------------------- /src/testing/mock-firebase-database.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { of } from 'rxjs/observable/of' 3 | 4 | @Injectable() 5 | export class MockFirebaseDatabaseService { 6 | get() { 7 | return of('test') 8 | } 9 | getList() { 10 | return of([]) 11 | } 12 | getListKeyed() { 13 | return of([]) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "declaration": false, 10 | "noImplicitAny": true, 11 | "noImplicitUseStrict": false, 12 | "strictNullChecks": true, 13 | "noEmitHelpers": false, 14 | "noLib": false, 15 | "noUnusedLocals": true, 16 | "outDir": "dist/", 17 | "importHelpers": true, 18 | "allowSyntheticDefaultImports": true, 19 | "sourceMap": true, 20 | "lib": ["es6", "dom"], 21 | "skipLibCheck": true 22 | }, 23 | "include": [ 24 | "./**/**.ts", 25 | "../tools/manual-typings/**/**.ts" 26 | ], 27 | "exclude": [ 28 | "client/main.aot-prod.ts", 29 | "client/main.aot.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "../node_modules/codelyzer", 4 | "../tools/tslint-rules" 5 | ], 6 | "extends": [ 7 | "angular-tslint-rules", 8 | "tslint-immutable" 9 | ], 10 | "rules": { 11 | "i18n": [false], 12 | "newline-per-chained-call": [false], 13 | "no-unnecessary-class": [false], 14 | "no-var-keyword": true, 15 | "no-let": false, 16 | "no-parameter-reassignment": false, 17 | "readonly-keyword": false, 18 | "readonly-array": false, 19 | "no-object-mutation": false, 20 | "no-delete": true, 21 | "no-method-signature": false, 22 | "typedef": [ 23 | false 24 | ], 25 | "angular-whitespace": [ 26 | true, 27 | "check-interpolation", 28 | "check-semicolon" 29 | ], 30 | "space-within-parens": [ 31 | false 32 | ], 33 | "object-literal-key-quotes": [ 34 | false 35 | ], 36 | "semicolon": [ 37 | true, 38 | "never" 39 | ], 40 | "align": [ 41 | false 42 | ], 43 | "no-console": [ 44 | false 45 | ], 46 | "curly": false, 47 | "member-ordering": [ 48 | false 49 | ], 50 | "member-access": [ 51 | false, 52 | "no-public" 53 | ], 54 | "newline-before-return": false, 55 | "directive-selector": [ 56 | true, 57 | "attribute", 58 | [ 59 | "app", 60 | "pm" 61 | ], 62 | "camelCase" 63 | ], 64 | "component-selector": [ 65 | true, 66 | "element", 67 | [ 68 | "pm", 69 | "app", 70 | "test", 71 | "i18n" 72 | ], 73 | "kebab-case" 74 | ], 75 | "use-view-encapsulation": false, 76 | "pipe-naming": [ 77 | true, 78 | "camelCase", 79 | "pm" 80 | ], 81 | "array-type": [ 82 | false 83 | ], 84 | "validate-decorators": [true, { 85 | "Component": { 86 | "changeDetection": "\\.OnPush$" 87 | } 88 | }, "**/!(*.spec).ts"] 89 | } 90 | } -------------------------------------------------------------------------------- /tools/config/app.config.ts: -------------------------------------------------------------------------------- 1 | export interface EnvConfig { 2 | name: string, 3 | description: string, 4 | firebase: { 5 | appName: string 6 | config: { 7 | apiKey: string 8 | authDomain: string 9 | databaseURL: string 10 | projectId: string 11 | storageBucket: string 12 | messagingSenderId: string 13 | } 14 | } 15 | host: string 16 | env?: string 17 | endpoints?: { 18 | api: string, 19 | websocketServer: string 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tools/config/build.ci.replace.ts: -------------------------------------------------------------------------------- 1 | import { argv } from 'yargs' 2 | 3 | // use this to replace values in your env/configs using environment variables from your CI/deployment system 4 | export const OVERRIDES = argv['ci-vars'] ? 5 | { 6 | host: `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`, 7 | endpoints: { 8 | api: `https://${process.env.HEROKU_APP_NAME}.herokuapp.com/api`, 9 | websocketServer: `wss://${process.env.HEROKU_APP_NAME}.herokuapp.com` 10 | } 11 | } : {} -------------------------------------------------------------------------------- /tools/config/build.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Dependency } from '../plugins/web-index' 2 | 3 | export interface BuildConfiguration { 4 | dependencies: Dependency[] 5 | baseHref: string 6 | faviconSource: string 7 | outputDir: string 8 | sourceDir: string 9 | prodOutDir: string 10 | assetParentDir: string 11 | minifyIndex: boolean 12 | skipFaviconGenerationOnDev: boolean 13 | toolsDir: string 14 | browserSyncPort: number 15 | host: string 16 | port: number 17 | } 18 | -------------------------------------------------------------------------------- /tools/env/base.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from '../config/app.config'; 2 | 3 | const BaseConfig: EnvConfig = { 4 | name: 'Fusebox Angular Universal Starter', 5 | description: 'Fusebox Angular Universal Starter', 6 | endpoints: { 7 | api: 'http://localhost:8000/api', 8 | websocketServer: 'ws://localhost:8001' 9 | }, 10 | firebase: { 11 | appName: 'fuse-angular-universal-starter', 12 | config: { 13 | apiKey: 'AIzaSyDfE1owJZCbvasXieCKjMoGZddRhqcp7RM', 14 | authDomain: 'fuse-angular-universal-s-67402.firebaseapp.com', 15 | databaseURL: 'https://fuse-angular-universal-s-67402.firebaseio.com', 16 | projectId: 'fuse-angular-universal-s-67402', 17 | storageBucket: 'fuse-angular-universal-s-67402.appspot.com', 18 | messagingSenderId: '883416191164' 19 | } 20 | }, 21 | host: 'http://localhost:8000' 22 | }; 23 | 24 | export = BaseConfig; -------------------------------------------------------------------------------- /tools/env/dev.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from '../config/app.config'; 2 | import * as base from './base'; 3 | 4 | const DevConfig: EnvConfig = { 5 | ...base, 6 | env: 'dev' 7 | }; 8 | 9 | export = DevConfig; -------------------------------------------------------------------------------- /tools/env/e2e.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from '../config/app.config'; 2 | import * as base from './base'; 3 | 4 | const DevConfig: EnvConfig = { 5 | ...base, 6 | env: 'e2e', 7 | endpoints: { 8 | api: 'http://localhost:8000/api', 9 | websocketServer: 'ws://localhost:80001' 10 | }, 11 | }; 12 | 13 | export = DevConfig; -------------------------------------------------------------------------------- /tools/env/prod.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from '../config/app.config'; 2 | import * as base from './base'; 3 | 4 | const ProdConfig: EnvConfig = { 5 | ...base, 6 | env: 'prod', 7 | host: "https://angular.patrickmichalina.com" 8 | }; 9 | 10 | export = ProdConfig; -------------------------------------------------------------------------------- /tools/manual-typings/README.md: -------------------------------------------------------------------------------- 1 | # Manual Typings 2 | 3 | When typings are not availabnle as part of the npm package or using @types repository, you can manual define the typed definitions for the javascript package. -------------------------------------------------------------------------------- /tools/manual-typings/project/sample.package.d.ts: -------------------------------------------------------------------------------- 1 | // declare module "moment/moment" { 2 | // export = moment; 3 | // } -------------------------------------------------------------------------------- /tools/manual-typings/seed/bunyan.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@expo/bunyan' { 2 | import * as bunyan from 'bunyan'; 3 | export = bunyan; 4 | } -------------------------------------------------------------------------------- /tools/manual-typings/seed/hash-files.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'hash-files' { 2 | export function sync(options?: any): string; 3 | } -------------------------------------------------------------------------------- /tools/manual-typings/seed/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /tools/manual-typings/seed/loglevel-std-streams.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'loglevel-std-streams' { 2 | interface ILoglevelStdStreams { 3 | (log: any): void; 4 | } 5 | 6 | const loglevelStdStreams: ILoglevelStdStreams; 7 | export = loglevelStdStreams; 8 | } -------------------------------------------------------------------------------- /tools/plugins/ng-aot.ts: -------------------------------------------------------------------------------- 1 | import { WorkFlowContext } from 'fuse-box/src/core/WorkflowContext'; 2 | import { File } from 'fuse-box/src/core/File'; 3 | 4 | import { } from '@angular/angular-cli' 5 | 6 | export interface NgAotPluginOptions { 7 | 8 | } 9 | 10 | export class NgAotPluginClass { 11 | public test: RegExp = /.ts/; 12 | 13 | constructor(options: NgAotPluginOptions = {}) { 14 | console.log(options); 15 | } 16 | 17 | public init(context: WorkFlowContext) { 18 | 19 | } 20 | 21 | public transform(file: File) { 22 | file.loadContents(); 23 | } 24 | } 25 | 26 | export const NgLazyPlugin = (options: NgAotPluginOptions = {}) => new NgAotPluginClass(options); 27 | -------------------------------------------------------------------------------- /tools/plugins/pwa-fused.ts: -------------------------------------------------------------------------------- 1 | import { WorkFlowContext } from 'fuse-box/src/core/WorkflowContext'; 2 | const pwaManifest = require('@pwa/manifest') 3 | 4 | export interface PwaFusedPluginOptions { 5 | distPath?: string, 6 | manifest?: { 7 | name?: string 8 | short_name?: string 9 | lang?: string 10 | start_url?: string 11 | display?: string, 12 | theme_color?: string, 13 | background_color?: string 14 | orientation?: string 15 | } 16 | } 17 | 18 | export class PwaFusedPluginClass { 19 | public dependencies = ['@pwa/manifest'] 20 | 21 | constructor(private opts: PwaFusedPluginOptions = {}) { 22 | this.opts = { 23 | distPath: 'dist', 24 | manifest: { 25 | name: 'Fusebox Angular Universal Starter', 26 | short_name: 'Angular Universal', 27 | lang: 'en', 28 | start_url: '/', 29 | display: 'standalone', 30 | theme_color: '#FFFFFF', 31 | background_color: '#3F51B5', 32 | orientation: 'natural' 33 | }, 34 | // ...opts, 35 | } as PwaFusedPluginOptions 36 | } 37 | 38 | public preBundle(context: WorkFlowContext) { 39 | if (context.bundle) { 40 | pwaManifest(this.opts.manifest).then((manifest: any) => { 41 | pwaManifest.write(this.opts.distPath || `${context.appRoot}/${this.opts.distPath}`, manifest); 42 | }); 43 | } 44 | } 45 | } 46 | 47 | export const PwaFusedPlugin = (options?: PwaFusedPluginOptions) => new PwaFusedPluginClass(options); -------------------------------------------------------------------------------- /tools/scripts/post-merge.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # MIT © Sindre Sorhus - sindresorhus.com 3 | 4 | # git hook to run a command after `git pull` if a specified file was changed 5 | changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)" 6 | 7 | check_run() { 8 | echo "$changed_files" | grep --quiet "$1" && eval "$2" 9 | } 10 | 11 | # run `npm install` if package.json changed 12 | check_run package.json "npm install" -------------------------------------------------------------------------------- /tools/scripts/replace.ts: -------------------------------------------------------------------------------- 1 | const jsdom = require("jsdom"); 2 | const { JSDOM } = jsdom; 3 | 4 | export const replace = function (file: any, key: string, replace: string) { 5 | const dom = new JSDOM(file); 6 | 7 | dom.window.document.querySelectorAll('[data-build-replace]').forEach((thing: any) => { 8 | const split = thing.getAttribute('data-build-replace').split(':'); 9 | if (key === split[0]) { 10 | thing[split[1]] = replace; 11 | thing.removeAttribute('data-build-replace') 12 | } 13 | }); 14 | 15 | return dom.serialize(); 16 | } 17 | 18 | export const replaceByQuery = function (file: any, query: string, key: string, replace: string) { 19 | const dom = new JSDOM(file); 20 | 21 | dom.window.document.querySelectorAll(query).forEach((thing: any) => { 22 | thing[key] = replace; 23 | }); 24 | 25 | return dom.serialize(); 26 | } 27 | 28 | export const prefixByQuery = function (file: any, query: string, key: string, prefix: string) { 29 | const dom = new JSDOM(file); 30 | 31 | dom.window.document.querySelectorAll(query).forEach((thing: any) => { 32 | if (!(thing[key] as string).includes('https') || !(thing[key] as string).includes('http')) { 33 | thing[key] = `${prefix}${thing[key]}`; 34 | } 35 | }); 36 | 37 | return dom.serialize(); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /tools/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | readdirSync(join(__dirname, 'seed')).forEach(file => require('./seed/' + file)); 5 | readdirSync(join(__dirname, 'project')).forEach(file => require('./project/' + file)); 6 | -------------------------------------------------------------------------------- /tools/tasks/project/sample.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box'; 2 | import { taskName } from '../../config/build.config'; 3 | 4 | 5 | // An example task stub 6 | Sparky.task(taskName(__filename), () => { 7 | console.log('Sample Task!') 8 | }); -------------------------------------------------------------------------------- /tools/tasks/seed/assets.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), () => Sparky.src('./assets/**/!(favicon).*', { base: `./${BUILD_CONFIG.assetParentDir}` }) 5 | .dest(`./${BUILD_CONFIG.outputDir}`)) 6 | -------------------------------------------------------------------------------- /tools/tasks/seed/banner.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { SparkyFile } from 'fuse-box/src/sparky/SparkyFile' 3 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 4 | 5 | Sparky.task(taskName(__filename), () => Sparky.src(`${BUILD_CONFIG.toolsDir}/config/console.banner.256.txt`) 6 | .file('console.banner.256.txt', (file: SparkyFile) => { 7 | file.read() 8 | console.log(file.contents.toString()) 9 | })) 10 | -------------------------------------------------------------------------------- /tools/tasks/seed/changelog.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | import { createWriteStream } from 'fs' 4 | // tslint:disable-next-line:no-require-imports 5 | const conventionalChangelog = require('conventional-changelog') 6 | 7 | Sparky.task(taskName(__filename), () => { 8 | // heroku does not clone the git repo, so no meta data can be found :( 9 | if (process.env.HEROKU) return Promise.resolve() 10 | 11 | return conventionalChangelog({ 12 | preset: 'angular', 13 | releaseCount: 0 14 | }).pipe(createWriteStream(`./${BUILD_CONFIG.outputDir}/CHANGELOG.md`)) 15 | }) 16 | -------------------------------------------------------------------------------- /tools/tasks/seed/clean.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), () => 5 | Sparky.src(`${BUILD_CONFIG.outputDir}`) 6 | .clean(`${BUILD_CONFIG.outputDir}`) 7 | .clean('.fusebox') 8 | .clean('.ngc') 9 | .clean('src/client/.aot')) 10 | -------------------------------------------------------------------------------- /tools/tasks/seed/config.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { taskName, ENV_CONFIG_INSTANCE } from '../../config/build.config' 3 | import { writeFileSync } from 'fs' 4 | 5 | Sparky.task(taskName(__filename), () => { 6 | writeFileSync('./src/config.json', JSON.stringify(ENV_CONFIG_INSTANCE, undefined, 2), { encoding: 'utf-8' }) 7 | }) 8 | -------------------------------------------------------------------------------- /tools/tasks/seed/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box'; 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config'; 3 | 4 | // copies font-awesome 5 | Sparky.task(taskName(__filename), () => 6 | Sparky.src('./**', { base: `./node_modules/font-awesome/fonts` }) 7 | .dest(`./${BUILD_CONFIG.outputDir}/assets/fonts/font-awesome`)); 8 | -------------------------------------------------------------------------------- /tools/tasks/seed/index.copy.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), () => Sparky.src('./index.html', { base: './src/client' }).dest(`./${BUILD_CONFIG.outputDir}`)) 5 | -------------------------------------------------------------------------------- /tools/tasks/seed/index.minify.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { SparkyFile } from 'fuse-box/src/sparky/SparkyFile' 3 | import { minify } from 'html-minifier' 4 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 5 | 6 | Sparky.task(taskName(__filename), () => { 7 | new Promise((resolve, reject) => { 8 | if (!BUILD_CONFIG.minifyIndex) resolve() 9 | 10 | return Sparky.src('./dist/index.html').file('index.html', (file: SparkyFile) => { 11 | file.read() 12 | file.setContent(minify(file.contents.toString(), { 13 | collapseWhitespace: true, 14 | removeComments: true, 15 | minifyJS: true 16 | })) 17 | file.save() 18 | resolve() 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /tools/tasks/seed/lint.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { taskName, typeHelper } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), () => typeHelper()) 5 | -------------------------------------------------------------------------------- /tools/tasks/seed/mk-dist.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | import { sync as mkdirp } from 'mkdirp' 4 | 5 | Sparky.task(taskName(__filename), () => { 6 | mkdirp(BUILD_CONFIG.outputDir) 7 | }) 8 | -------------------------------------------------------------------------------- /tools/tasks/seed/ngc.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { main as ngc } from '@angular/compiler-cli/src/main' 3 | import { taskName } from '../../config/build.config' 4 | 5 | Sparky.task(taskName(__filename), () => { 6 | return ngc(['tsconfig-aot.json']) 7 | }) 8 | -------------------------------------------------------------------------------- /tools/tasks/seed/ngsw-json.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | // tslint:disable-next-line:no-require-imports 4 | const nrc = require('node-run-cmd') 5 | 6 | Sparky.task(taskName(__filename), () => { 7 | return nrc.run(`node_modules/.bin/ngsw-config ${BUILD_CONFIG.outputDir} ./src/client/ngsw.json`) 8 | }) 9 | 10 | -------------------------------------------------------------------------------- /tools/tasks/seed/ngsw-worker.min.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { SparkyFile } from 'fuse-box/src/sparky/SparkyFile' 3 | import { taskName } from '../../config/build.config' 4 | // tslint:disable-next-line:no-require-imports 5 | const uglifyJS = require('uglify-es') 6 | 7 | Sparky.task(taskName(__filename), () => { 8 | return Sparky.src(['./dist/ngsw-worker.js']).file('ngsw-worker.js', (file: SparkyFile) => { 9 | file.read() 10 | const result = uglifyJS.minify(file.contents.toString()) 11 | file.setContent(result.code) 12 | file.save() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tools/tasks/seed/ngsw-worker.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), () => { 5 | return Sparky.src(['ngsw-worker.js'], { base: './node_modules/@angular/service-worker' }).dest(`./${BUILD_CONFIG.outputDir}`) 6 | }) 7 | -------------------------------------------------------------------------------- /tools/tasks/seed/ngsw.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), ['ngsw-worker', 'ngsw-worker.min', 'ngsw-json'], () => undefined) 5 | -------------------------------------------------------------------------------- /tools/tasks/seed/sass.files.ts: -------------------------------------------------------------------------------- 1 | import { sync as glob } from 'glob' 2 | import { Sparky } from 'fuse-box' 3 | import { SparkyFile } from 'fuse-box/src/sparky/SparkyFile' 4 | import { readFileSync } from 'fs' 5 | import { taskName } from '../../config/build.config' 6 | import { ConfigurationTransformer, Dependency } from '../../plugins/web-index' 7 | // tslint:disable-next-line:no-require-imports 8 | import hash = require('string-hash') 9 | 10 | Sparky.task(taskName(__filename), () => { 11 | const css = glob('./dist/css/**/*.css').map(a => { 12 | return { 13 | hash: hash(readFileSync(a).toString()), 14 | name: a.replace('./dist', '') 15 | } 16 | }) 17 | 18 | return Sparky.src('./dist/index.html').file('index.html', (file: SparkyFile) => { 19 | file.read() 20 | 21 | const transformer = new ConfigurationTransformer() 22 | const deps: Dependency[] = css.map(c => { 23 | return { 24 | inHead: true, 25 | order: 1, 26 | element: 'link', 27 | attributes: { 28 | rel: 'stylesheet', 29 | href: `${c.name}` 30 | } 31 | } as Dependency 32 | }) 33 | 34 | const html = transformer.applyTransform(deps, file.contents.toString()) 35 | file.setContent(html) 36 | file.save() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tools/tasks/seed/sass.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-require-imports 2 | import { Sparky } from 'fuse-box' 3 | import { isBuildServer, isProdBuild, taskName } from '../../config/build.config' 4 | import { renderSync } from 'node-sass' 5 | import { unlinkSync, writeFileSync, readFileSync } from 'fs' 6 | import { sync as mkdirp } from 'mkdirp' 7 | import { sync as glob } from 'glob' 8 | import { ConfigurationTransformer } from '../../plugins/web-index' 9 | import * as cleanCss from 'clean-css' 10 | import hash = require('string-hash') 11 | 12 | Sparky.task(taskName(__filename), () => { 13 | let src 14 | mkdirp('./dist/css') 15 | 16 | const sass = () => { 17 | const result = renderSync({ 18 | file: './src/client/styles/main.scss', 19 | }) 20 | const hashed = hash(result.css.toString()) 21 | 22 | return { 23 | css: result.css, 24 | hashed 25 | } 26 | } 27 | 28 | const process = () => { 29 | const _sass = sass() 30 | const name = 'main.css' 31 | 32 | glob('./dist/css/main-*').forEach(file => unlinkSync(file)) 33 | writeFileSync(`./dist/css/${name}`, _sass.css) 34 | 35 | const indexPath = './dist/index.html' 36 | const index = readFileSync(indexPath) 37 | const transformer = new ConfigurationTransformer(); 38 | const html = transformer.applyTransform([{ 39 | inHead: true, 40 | order: 1, 41 | element: 'style', 42 | content: new cleanCss({}).minify(_sass.css.toString()).styles, 43 | attributes: { 44 | id: 'primary-styles' 45 | } 46 | }], index.toString()); 47 | writeFileSync(indexPath, html); 48 | } 49 | 50 | src = isProdBuild || isBuildServer 51 | ? Sparky.src('src/client/styles/**/**/*.scss').file('main.scss', process) 52 | : Sparky.watch('src/client/styles/**/**/*.scss').file('main.scss', process) 53 | 54 | return src 55 | }) 56 | -------------------------------------------------------------------------------- /tools/tasks/seed/serve.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), [ 5 | 'clean', 6 | 'mk-dist', 7 | 'config', 8 | 'index.copy', 9 | // 'favicons', 10 | 'fonts', 11 | 'changelog', 12 | 'web', 13 | 'assets', 14 | 'sass', 15 | 'build.universal', 16 | 'ngsw', 17 | 'banner' 18 | ], () => undefined) 19 | -------------------------------------------------------------------------------- /tools/tasks/seed/web.ts: -------------------------------------------------------------------------------- 1 | import { Sparky } from 'fuse-box' 2 | import { BUILD_CONFIG, taskName } from '../../config/build.config' 3 | 4 | Sparky.task(taskName(__filename), () => 5 | Sparky.src('web/**/*.*', { base: `${BUILD_CONFIG.toolsDir}` }) 6 | .dest(`./${BUILD_CONFIG.outputDir}`)) 7 | -------------------------------------------------------------------------------- /tools/test/jest.e2e-setup.ts: -------------------------------------------------------------------------------- 1 | import * as Nightmare from 'nightmare' 2 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 125000 3 | 4 | // tslint:disable:no-require-imports 5 | const browser = require('nightmare')({ 6 | show: false 7 | }) as Nightmare 8 | 9 | const baseUrl = 'http://localhost:8000' 10 | 11 | export { browser, baseUrl } 12 | -------------------------------------------------------------------------------- /tools/test/jest.mocks.ts: -------------------------------------------------------------------------------- 1 | const mock = () => { 2 | let storage = {}; 3 | return { 4 | getItem: (key: string) => key in storage ? (storage)[key] : null, 5 | setItem: (key: string, value: any) => (storage)[key] = value || '', 6 | removeItem: (key: string) => delete (storage)[key], 7 | clear: () => storage = {}, 8 | }; 9 | }; 10 | // Object.defineProperty(window, 'Hammer', { value: {} }); 11 | Object.defineProperty(window, 'CSS', { value: mock() }); 12 | Object.defineProperty(window, 'matchMedia', { value: jest.fn(() => ({ matches: true })) }); 13 | Object.defineProperty(window, 'localStorage', { value: mock() }); 14 | Object.defineProperty(window, 'sessionStorage', { value: mock() }); 15 | Object.defineProperty(window, 'getComputedStyle', { 16 | value: () => { 17 | return { 18 | display: 'none', 19 | appearance: ['-webkit-appearance'] 20 | }; 21 | } 22 | }); 23 | 24 | // For Angular Material 25 | Object.defineProperty(document.body.style, 'transform', { 26 | value: () => { 27 | return { 28 | enumerable: true, 29 | configurable: true 30 | }; 31 | }, 32 | }); 33 | 34 | // For Angular Material 35 | (window as any).Hammer = require('hammerjs') 36 | -------------------------------------------------------------------------------- /tools/test/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | import './jest.mocks'; 3 | -------------------------------------------------------------------------------- /tools/web/ping.html: -------------------------------------------------------------------------------- 1 | Ok -------------------------------------------------------------------------------- /tools/web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /tsconfig-aot.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "lib": [ 10 | "es2015", 11 | "dom" 12 | ], 13 | "noImplicitAny": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "typeRoots": [ 16 | "./node_modules/@types/", 17 | "./node_modules/@angular/material/" 18 | ], 19 | "outDir": ".ngc" 20 | }, 21 | "include": [ 22 | "./src/client/main.ts", 23 | "./src/client/app/**/*.module.ts" 24 | ], 25 | "angularCompilerOptions": { 26 | "genDir": "src/client/.aot", 27 | "skipMetadataEmit": true 28 | } 29 | } -------------------------------------------------------------------------------- /tsconfig-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "pretty": true, 12 | "allowUnreachableCode": false, 13 | "allowUnusedLabels": false, 14 | "noImplicitAny": false, 15 | "noImplicitReturns": true, 16 | "noImplicitUseStrict": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "typeRoots": [ 19 | "../../node_modules/@types", 20 | "../../node_modules" 21 | ], 22 | "outDir": ".e2e", 23 | "types": [ 24 | "node", 25 | "protractor", 26 | "jest" 27 | ] 28 | }, 29 | "include": [ 30 | "./src/client/app/**/*e2e-spec.ts" 31 | ], 32 | "compileOnSave": false 33 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "declaration": false, 10 | "noImplicitAny": true, 11 | "noImplicitUseStrict": false, 12 | "strictNullChecks": true, 13 | "noEmitHelpers": false, 14 | "noLib": false, 15 | "noUnusedLocals": true, 16 | "outDir": "dist/", 17 | "allowSyntheticDefaultImports": false, 18 | "sourceMap": true, 19 | "lib": ["es6", "dom"], 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true 22 | }, 23 | "exclude": [ 24 | "./.vscode", 25 | "./.fusebox", 26 | "./.ngc", 27 | "./dist", 28 | "./node_modules", 29 | "./coverage" 30 | ] 31 | } --------------------------------------------------------------------------------