├── .circleci └── config.yml ├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .htmlhintrc ├── .stylelintrc ├── .travis.yml ├── .yo-rc.json ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── angular.json ├── assets ├── MDPI_SCREEN.png ├── angular-material-extensions-logo.png └── angular-material-extensions-logo.svg ├── commitlint.config.js ├── coverage.lcov ├── docs ├── analytics.md ├── backend-proxy.md ├── coding-guides │ ├── angular.md │ ├── e2e-tests.md │ ├── html.md │ ├── sass.md │ ├── typescript.md │ └── unit-tests.md ├── corporate-proxy.md ├── i18n.md ├── readme.md ├── routing.md └── updating.md ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── ngsw-config.json ├── package-lock.json ├── package.json ├── prerender.ts ├── proxy.conf.js ├── server.ts ├── src ├── app │ ├── about │ │ ├── about-routing.module.ts │ │ ├── about.component.html │ │ ├── about.component.scss │ │ ├── about.component.spec.ts │ │ ├── about.component.ts │ │ └── about.module.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.server.module.ts │ ├── components │ │ ├── about │ │ │ ├── about.component.html │ │ │ ├── about.component.scss │ │ │ ├── about.component.spec.ts │ │ │ └── about.component.ts │ │ ├── home │ │ │ ├── home-routing.module.ts │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ ├── home.module.ts │ │ │ ├── quote.service.spec.ts │ │ │ └── quote.service.ts │ │ ├── project-dialog │ │ │ ├── project-dialog.component.html │ │ │ ├── project-dialog.component.scss │ │ │ ├── project-dialog.component.spec.ts │ │ │ └── project-dialog.component.ts │ │ └── shell │ │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ └── header.component.ts │ │ │ ├── shell.component.html │ │ │ ├── shell.component.scss │ │ │ ├── shell.component.spec.ts │ │ │ ├── shell.component.ts │ │ │ ├── shell.module.spec.ts │ │ │ ├── shell.module.ts │ │ │ ├── shell.service.spec.ts │ │ │ └── shell.service.ts │ ├── core │ │ ├── core.module.ts │ │ ├── http │ │ │ ├── api-prefix.interceptor.spec.ts │ │ │ ├── api-prefix.interceptor.ts │ │ │ ├── cache.interceptor.spec.ts │ │ │ ├── cache.interceptor.ts │ │ │ ├── error-handler.interceptor.spec.ts │ │ │ ├── error-handler.interceptor.ts │ │ │ ├── http-cache.service.spec.ts │ │ │ ├── http-cache.service.ts │ │ │ ├── http.service.spec.ts │ │ │ └── http.service.ts │ │ ├── i18n.service.spec.ts │ │ ├── i18n.service.ts │ │ ├── index.ts │ │ ├── logger.service.spec.ts │ │ ├── logger.service.ts │ │ └── route-reusable-strategy.ts │ ├── home │ │ ├── home-routing.module.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ ├── home.component.ts │ │ ├── home.module.ts │ │ ├── quote.service.spec.ts │ │ └── quote.service.ts │ ├── material.module.ts │ ├── shared │ │ ├── index.ts │ │ ├── loader │ │ │ ├── loader.component.html │ │ │ ├── loader.component.scss │ │ │ ├── loader.component.spec.ts │ │ │ └── loader.component.ts │ │ └── shared.module.ts │ └── shell │ │ ├── header │ │ ├── header.component.html │ │ ├── header.component.scss │ │ ├── header.component.spec.ts │ │ └── header.component.ts │ │ ├── shell.component.html │ │ ├── shell.component.scss │ │ ├── shell.component.spec.ts │ │ ├── shell.component.ts │ │ ├── shell.module.spec.ts │ │ ├── shell.module.ts │ │ ├── shell.service.spec.ts │ │ └── shell.service.ts ├── apple-touch-icon.png ├── assets │ ├── icons │ │ ├── facebook.svg │ │ ├── github-circle.svg │ │ ├── linkedin.svg │ │ └── twitter.svg │ ├── images │ │ └── icons │ │ │ ├── icon-128x128.png │ │ │ ├── icon-144x144.png │ │ │ ├── icon-152x152.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-384x384.png │ │ │ ├── icon-512x512.png │ │ │ ├── icon-72x72.png │ │ │ └── icon-96x96.png │ ├── img │ │ ├── portfolio │ │ │ ├── cabin.png │ │ │ ├── cake.png │ │ │ ├── circus.png │ │ │ ├── game.png │ │ │ ├── safe.png │ │ │ └── submarine.png │ │ └── profile.png │ ├── manifest.json │ ├── ngx-rocket-logo.png │ └── pdf │ │ └── graduate_software_engineer.pdf ├── browserslist ├── config.interface.ts ├── config.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.scss ├── main.server.ts ├── main.ts ├── manifest.json ├── polyfills.ts ├── robots.txt ├── test.ts ├── theme │ ├── theme-variables.scss │ └── theme.scss ├── translations │ ├── de-DE.json │ ├── en-US.json │ └── fr-FR.json ├── tsconfig.app.json ├── tsconfig.server.json ├── tsconfig.spec.json └── typings.d.ts ├── static.paths.ts ├── tsconfig.json ├── tslint.json └── webpack.server.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | job_defaults: &job_defaults 4 | docker: 5 | - image: circleci/node:10-browsers 6 | environment: 7 | CHROME_BIN: '/usr/bin/google-chrome' 8 | working_directory: ~/project/repo 9 | 10 | cache_key: &cache_key angular-material-extension/freelancer-theme-deps-cache-{{ .Branch }}-{{ checksum "package-lock.json" }} 11 | dist_key: &dist_key angular-material-extension/freelancer-theme-dist-{{ .Revision }} 12 | 13 | jobs: 14 | install: 15 | <<: *job_defaults 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | key: *cache_key 20 | - run: 21 | name: install-dependencies 22 | command: npm ci 23 | - save_cache: 24 | key: *cache_key 25 | paths: 26 | - node_modules 27 | 28 | lint: 29 | <<: *job_defaults 30 | steps: 31 | - checkout 32 | - restore_cache: 33 | key: *cache_key 34 | - run: 35 | name: lint 36 | command: npm run lint 37 | 38 | test-app: 39 | <<: *job_defaults 40 | steps: 41 | - checkout 42 | - restore_cache: 43 | key: *cache_key 44 | - restore_cache: 45 | key: *dist_key 46 | - run: 47 | name: test 48 | command: npm run test:ci 49 | 50 | codecov: 51 | <<: *job_defaults 52 | steps: 53 | - checkout 54 | - restore_cache: 55 | key: *cache_key 56 | - restore_cache: 57 | key: *dist_key 58 | - run: 59 | name: codecov 60 | command: npx codecov 61 | 62 | build-prerender-app: 63 | <<: *job_defaults 64 | steps: 65 | - checkout 66 | - restore_cache: 67 | key: *cache_key 68 | - run: 69 | name: test 70 | command: npm run build:prerender:ci 71 | - save_cache: 72 | key: *dist_key 73 | paths: 74 | - dist 75 | 76 | release: 77 | <<: *job_defaults 78 | steps: 79 | - checkout 80 | - restore_cache: 81 | key: *cache_key 82 | - restore_cache: 83 | key: *dist_key 84 | - run: 85 | name: release 86 | command: npm run semantic-release || true 87 | 88 | deploy-app: 89 | <<: *job_defaults 90 | steps: 91 | - checkout 92 | - restore_cache: 93 | key: *cache_key 94 | - restore_cache: 95 | key: *dist_key 96 | - run: 97 | name: deploy-app 98 | command: npm run deploy:demo 99 | 100 | workflows: 101 | version: 2 102 | build-test-release: 103 | jobs: 104 | - install 105 | - lint: 106 | requires: 107 | - install 108 | # - test-app: 109 | # requires: 110 | # - install 111 | # - codecov: 112 | # requires: 113 | # - test-app 114 | - release: 115 | requires: 116 | - lint 117 | filters: 118 | branches: 119 | only: master 120 | - build-prerender-app: 121 | requires: 122 | - release 123 | # - lint 124 | # - test-app 125 | - deploy-app: 126 | requires: 127 | - build-prerender-app 128 | filters: 129 | branches: 130 | only: master 131 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | max_line_length = 120 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: angular-material-extensions 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | liberapay: # Replace with a single Liberapay username 9 | issuehunt: # Replace with a single IssueHunt username 10 | otechie: # Replace with a single Otechie username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | custom: ['https://github.com/AnthonyNahas','anahas.de'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # Dependencies 9 | /node_modules 10 | 11 | # Cordova 12 | /www 13 | /plugins 14 | /platforms 15 | 16 | # IDEs and editors 17 | .idea/* 18 | !.idea/runConfigurations/ 19 | !.idea/codeStyleSettings.xml 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | xcuserdata/ 26 | *.sublime-workspace 27 | 28 | # IDE - VSCode 29 | .vscode/* 30 | !.vscode/settings.json 31 | !.vscode/tasks.json 32 | !.vscode/launch.json 33 | !.vscode/extensions.json 34 | 35 | # Maven 36 | /target 37 | /log 38 | 39 | # Misc 40 | /.sass-cache 41 | /connect.lock 42 | /coverage 43 | /libpeerconnection.log 44 | npm-debug.log 45 | yarn-error.log 46 | testem.log 47 | /typings 48 | /reports 49 | /src/translations/template.* 50 | /src/environments/.env.* 51 | 52 | # System Files 53 | .DS_Store 54 | Thumbs.db 55 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": false, 3 | "attr-lowercase": false, 4 | "attr-value-double-quotes": true, 5 | "tag-pair": true, 6 | "spec-char-escape": true, 7 | "id-unique": true, 8 | "src-not-empty": true, 9 | "attr-no-duplication": true, 10 | "title-require": true, 11 | "tag-self-close": true, 12 | "head-script-disabled": true, 13 | "doctype-html5": true, 14 | "id-class-value": "dash", 15 | "style-disabled": true, 16 | "inline-style-disabled": true, 17 | "inline-script-disabled": true, 18 | "space-tab-mixed-disabled": "true", 19 | "id-class-ad-disabled": true, 20 | "attr-unsafe-chars": true 21 | } 22 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-recommended-scss", 5 | "stylelint-config-prettier" 6 | ], 7 | "rules": { 8 | "font-family-name-quotes": "always-where-recommended", 9 | "function-url-quotes": [ 10 | "always", 11 | { 12 | "except": ["empty"] 13 | } 14 | ], 15 | "selector-attribute-quotes": "always", 16 | "string-quotes": "double", 17 | "max-nesting-depth": 3, 18 | "selector-max-compound-selectors": 3, 19 | "selector-max-specificity": "0,3,2", 20 | "declaration-no-important": true, 21 | "at-rule-no-vendor-prefix": true, 22 | "media-feature-name-no-vendor-prefix": true, 23 | "property-no-vendor-prefix": true, 24 | "selector-no-vendor-prefix": true, 25 | "value-no-vendor-prefix": true, 26 | "no-empty-source": null, 27 | "selector-class-pattern": "[a-z-]+", 28 | "selector-id-pattern": "[a-z-]+", 29 | "selector-max-id": 0, 30 | "selector-no-qualifying-type": true, 31 | "selector-max-universal": 0, 32 | "selector-pseudo-element-no-unknown": [ 33 | true, 34 | { 35 | "ignorePseudoElements": ["ng-deep"] 36 | } 37 | ], 38 | "unit-whitelist": ["px", "%", "em", "rem", "vw", "vh", "deg", "s"], 39 | "max-empty-lines": 2, 40 | "max-line-length": 120 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | #install google chrome, using addons 5 | addons: 6 | apt: 7 | sources: 8 | - google-chrome 9 | packages: 10 | - google-chrome-stable 11 | 12 | language: node_js 13 | node_js: 14 | - '10' 15 | 16 | branches: 17 | only: 18 | - master 19 | - /^greenkeeper/.*$/ 20 | 21 | cache: 22 | directories: 23 | - node_modules 24 | 25 | before_script: 26 | - export DISPLAY=:99.0 27 | - sh -e /etc/init.d/xvfb start 28 | # - npm install --quiet -g gulp-cli 29 | 30 | #script: gulp test:ci 31 | script: 32 | # - ng test --watch false -cc 33 | # - npm run e2e 34 | - npm run build:prerender 35 | 36 | after_success: gulp coveralls 37 | 38 | deploy: 39 | provider: pages 40 | skip_cleanup: true 41 | github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard 42 | local_dir: dist 43 | on: 44 | branch: master 45 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-ngx-rocket": { 3 | "version": "5.3.0", 4 | "props": { 5 | "location": "path", 6 | "appName": "freelancer-theme", 7 | "target": ["web"], 8 | "pwa": true, 9 | "ui": "material", 10 | "layout": "simple", 11 | "auth": false, 12 | "lazy": true, 13 | "angulartics": true, 14 | "analyticsProvider": "ga", 15 | "googleAnalyticsAccount": "", 16 | "prettier": true, 17 | "projectName": "freelancer-theme", 18 | "packageManager": "npm", 19 | "mobile": [] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Anthony.na@hotmail.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### Bug Report or Feature Request (mark with an `x`) 6 | 7 | ``` 8 | - [ ] bug report -> please search issues before submitting 9 | - [ ] feature request 10 | ``` 11 | 12 | ### OS and Version? 13 | 14 | 17 | 18 | ### Versions 19 | 20 | 24 | 25 | ### Repro steps 26 | 27 | 32 | 33 | ### The log given by the failure 34 | 35 | 36 | 37 | ### Desired functionality 38 | 39 | 43 | 44 | ### Mention any other details that might be useful 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 angular-material-extensions 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 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "freelancer-theme": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/browser", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "tsConfig": "src/tsconfig.app.json", 24 | "polyfills": "src/polyfills.ts", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/apple-touch-icon.png", 28 | "src/robots.txt", 29 | "src/manifest.json", 30 | "src/assets" 31 | ], 32 | "styles": ["src/main.scss", "node_modules/material-design-icons/iconfont/material-icons.css"], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "serviceWorker": true, 47 | "fileReplacements": [ 48 | { 49 | "replace": "src/environments/environment.ts", 50 | "with": "src/environments/environment.prod.ts" 51 | } 52 | ] 53 | } 54 | } 55 | }, 56 | "server": { 57 | "builder": "@angular-devkit/build-angular:server", 58 | "options": { 59 | "outputPath": "dist/server", 60 | "main": "src/main.server.ts", 61 | "tsConfig": "src/tsconfig.server.json" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "fileReplacements": [ 66 | { 67 | "replace": "src/environments/environment.ts", 68 | "with": "src/environments/environment.prod.ts" 69 | } 70 | ] 71 | } 72 | } 73 | }, 74 | "serve": { 75 | "builder": "@angular-devkit/build-angular:dev-server", 76 | "options": { 77 | "browserTarget": "freelancer-theme:build" 78 | }, 79 | "configurations": { 80 | "production": { 81 | "browserTarget": "freelancer-theme:build:production" 82 | } 83 | } 84 | }, 85 | "extract-i18n": { 86 | "builder": "@angular-devkit/build-angular:extract-i18n", 87 | "options": { 88 | "browserTarget": "freelancer-theme:build" 89 | } 90 | }, 91 | "test": { 92 | "builder": "@angular-devkit/build-angular:karma", 93 | "options": { 94 | "main": "src/test.ts", 95 | "karmaConfig": "src/karma.conf.js", 96 | "polyfills": "src/polyfills.ts", 97 | "tsConfig": "src/tsconfig.spec.json", 98 | "scripts": [], 99 | "styles": ["src/main.scss", "node_modules/material-design-icons/iconfont/material-icons.css"], 100 | "assets": [ 101 | "src/favicon.ico", 102 | "src/apple-touch-icon.png", 103 | "src/robots.txt", 104 | "src/manifest.json", 105 | "src/assets" 106 | ] 107 | } 108 | }, 109 | "lint": { 110 | "builder": "@angular-devkit/build-angular:tslint", 111 | "options": { 112 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 113 | "exclude": ["**/node_modules/**"] 114 | } 115 | } 116 | } 117 | }, 118 | "freelancer-theme-e2e": { 119 | "root": "e2e", 120 | "projectType": "application", 121 | "architect": { 122 | "e2e": { 123 | "builder": "@angular-devkit/build-angular:protractor", 124 | "options": { 125 | "protractorConfig": "e2e/protractor.conf.js", 126 | "devServerTarget": "freelancer-theme:serve" 127 | } 128 | }, 129 | "lint": { 130 | "builder": "@angular-devkit/build-angular:tslint", 131 | "options": { 132 | "tsConfig": ["e2e/tsconfig.e2e.json"], 133 | "exclude": ["**/node_modules/**"] 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "defaultProject": "freelancer-theme" 140 | } 141 | -------------------------------------------------------------------------------- /assets/MDPI_SCREEN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/assets/MDPI_SCREEN.png -------------------------------------------------------------------------------- /assets/angular-material-extensions-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/assets/angular-material-extensions-logo.png -------------------------------------------------------------------------------- /assets/angular-material-extensions-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /coverage.lcov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/coverage.lcov -------------------------------------------------------------------------------- /docs/analytics.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | Analytics in this app are managed through the [Angulartics2](https://github.com/angulartics/angulartics2) library. 4 | 5 | It is already pre-configured to track page views, and provides examples to track events from both TypeScript code and HTML elements. 6 | Here is a quick usage documentation, you can read further information on the official website. 7 | 8 | ## Registering your provider 9 | 10 | Google Analytics is already registered as the project's analytics provider. 11 | Should you need to change the account identifier, you can do so in the call to `ga(...)` performed in the body of `index.html`. 12 | 13 | ## Tracking events 14 | 15 | ### Declarative event tracking 16 | 17 | The simplest way to do event tracking is by adding the attributes `angulartics2On`, `angularticsCategory` and `angularticsAction` to an HTML element. 18 | The homepage generated by the starter kit contains one such button. 19 | For reference, here is a UI-framework-agnostic example. 20 | 21 | ```html 22 | 29 | ``` 30 | 31 | ### Using the API 32 | 33 | As an example, the application already comes configured to track its startup through an event. 34 | You may use the example as reference: it can be found in the first lines of `ngOnInit()` in `app.component.ts`. 35 | 36 | To access the API, inject your provider : 37 | 38 | ```typescript 39 | constructor(... 40 | private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, 41 | ...) 42 | ``` 43 | 44 | You may then use the `eventTrack` function: 45 | 46 | ```typescript 47 | this.angulartics2GoogleAnalytics.eventTrack('Something happened', { 48 | category: 'My category' 49 | }); 50 | this.angulartics2GoogleAnalytics.eventTrack('Something else happened', { 51 | category: 'My other category', 52 | label: 'My custom label' 53 | }); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/backend-proxy.md: -------------------------------------------------------------------------------- 1 | # Backend proxy 2 | 3 | Usually when working on a web application you consume data from custom-made APIs. 4 | 5 | To ease development with our development server integrating live reload while keeping your backend API calls working, 6 | we also have setup a backend proxy to redirect API calls to whatever URL and port you want. This allows you: 7 | 8 | - To develop frontend features without the need to run an API backend locally 9 | - To use a local development server without [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) issues 10 | - To debug frontend code with data from a remote testing platform directly 11 | 12 | ## How to configure 13 | 14 | In the root folder you will find a `proxy.conf.js`, containing the backend proxy configuration. 15 | 16 | The interesting part is there: 17 | 18 | ```js 19 | const proxyConfig = [ 20 | { 21 | context: '/api', 22 | pathRewrite: { '^/api': '' }, 23 | target: 'http://api.icndb.com', 24 | changeOrigin: true 25 | } 26 | ]; 27 | ``` 28 | 29 | This is where you can setup one or more proxy rules. 30 | 31 | For the complete set of options, see the `http-proxy-middleware` 32 | [documentation](https://github.com/chimurai/http-proxy-middleware#options). 33 | 34 | ### Corporate proxy support 35 | 36 | To allow external API calls redirection through a corporate proxy, you will also find a `setupForCorporateProxy()` 37 | function in the proxy configuration file. By default, this method configures a corporate proxy agent based on the 38 | `HTTP_PROXY` environment variable, see the [corporate proxy documentation](corporate-proxy.md) for more details. 39 | 40 | If you need to, you can further customize this function to fit the network of your working environment. 41 | 42 | If your corporate proxy use a custom SSL certificate, your may need to add the `secure: false` option to your 43 | backend proxy configuration. 44 | -------------------------------------------------------------------------------- /docs/coding-guides/e2e-tests.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests coding guide 2 | 3 | End-to-end (E2E for short) tests are meant to test the behavior of your application, from start to finish. 4 | 5 | While unit tests are the first choice for catching bugs and regression on individual components, it is a good idea to 6 | complement them with test cases covering the integration between the individual components, hence the need for E2E 7 | tests. 8 | 9 | These tests use [Protractor](https://github.com/angular/protractor), which is a framework built for Angular on top of 10 | [Selenium](https://github.com/SeleniumHQ/selenium) to control browsers and simulate user inputs. 11 | [Jasmine](http://jasmine.github.io) is used as the base test framework. 12 | 13 | ## Good practices 14 | 15 | - Avoid whenever possible inter-dependencies between your E2E tests 16 | - Run E2E tests on your continuous integration server against different browsers 17 | - If you use an Agile methodology, cover each user story acceptance factors with an E2E test 18 | 19 | ## Page objects 20 | 21 | E2E tests should follow the _[Page Object](https://github.com/SeleniumHQ/selenium/wiki/PageObjects)_ pattern. 22 | 23 | #### What is a page object? 24 | 25 | A page object: 26 | 27 | - Models the objects on a page under test: 28 | - _Properties_ wrap page elements 29 | - _Methods_ wrap code that interacts with the page elements 30 | - Simplifies the test scripts 31 | - Reduces the amount of duplicated code 32 | 33 | If the UI changes, the fix only needs to be applied in one place. 34 | 35 | #### How to define a page object 36 | 37 | ```typescript 38 | // login.po.ts 39 | import { browser, element, by } from 'protractor'; 40 | 41 | export class LoginPage { 42 | emailInput = element(by.css('input[name=^"email"]')); 43 | passwordInput = element(by.css('input[name=^"password"]')); 44 | loginButton = element(by.css('button[(click)^="login"]')); 45 | registerButton = element(by.css('button[(click)^="register"]')); 46 | 47 | navigateTo() { 48 | return browser.get('/'); 49 | } 50 | 51 | getGreetingText() { 52 | return element(by.css('.greeting')).getText(); 53 | } 54 | } 55 | ``` 56 | 57 | #### How to use a page object 58 | 59 | ```typescript 60 | // login.e2e-spec.ts 61 | import { LoginPage } from './login.po'; 62 | 63 | describe('Login', () => { 64 | let page: LoginPage; 65 | 66 | beforeEach(() => { 67 | page = new LoginPage(); 68 | page.navigateTo(); 69 | }); 70 | 71 | it('should navigate to the register page when the register button is clicked', () => { 72 | page.registerButton.click(); 73 | 74 | expect(browser.getCurrentUrl()).toContain('/register'); 75 | }); 76 | 77 | it('should allow a user to log in', () => { 78 | page.emailInput.sendKeys('test@mail.com'); 79 | page.passwordInput.sendKeys('abc123'); 80 | page.loginButton.click(); 81 | 82 | expect(page.getGreetingText()).toContain('Welcome, Test User'); 83 | }); 84 | }); 85 | ``` 86 | 87 | ## Credits 88 | 89 | Parts of this guide were freely inspired by this 90 | [presentation](https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ). 91 | -------------------------------------------------------------------------------- /docs/coding-guides/html.md: -------------------------------------------------------------------------------- 1 | # HTML coding guide 2 | 3 | ## Naming conventions 4 | 5 | - Everything should be named in `kebab-case` (lowercase words separated with a `-`): tags, attributes, IDs, etc, 6 | **except for everything bound to Angular** such variables, directives or events which should be in `camelCase` 7 | - File names should always be in `kebab-case` 8 | 9 | ## Coding rules 10 | 11 | - Use HTML5 doctype: `` 12 | - Use HTML [semantic elements](https://developer.mozilla.org/docs/Web/HTML/Sections_and_Outlines_of_an_HTML5_document) 13 | - Use double quotes `"` around attribute values in tags 14 | - Use a new line for every block, list, or table element, and indent every such child element 15 | - Clearly Separate structure (HTML) from presentation (CSS) from behavior (JavaScript): 16 | - Never use inline CSS or JavaScript 17 | - Keep any logic out of the HTML 18 | - `type` attribute for stylesheets and script tags should be omitted 19 | 20 | ## Common pitfalls 21 | 22 | - **Block**-type tags cannot be nested inside **inline**-type tags: a `
` tag cannot be nested in a ``. 23 | This rule also applies regarding the `display` value of an element. 24 | - HTML is **not** XML: empty tags cannot be self-closing and will result in improper results 25 | - `
` will be interpreted as a simple `
` without closing tag! 26 | - The only tags that allows self-closing are the one that does not require a closing tag in first place: 27 | these are the void elements that do not not accept content `
`, `
`, ``, ``, ``, `` 28 | (and others). 29 | 30 | ## Templates 31 | 32 | In accordance with the [Angular style guide](https://angular.io/guide/styleguide), HTML templates should be extracted in 33 | separate files, when more than 3 lines. 34 | 35 | Only use inline templates sparingly in very simple components with less than 3 lines of HTML. 36 | 37 | ## Enforcement 38 | 39 | Coding rules enforcement and basic sanity checks are done in this project by [HTMLHint](http://htmlhint.com). 40 | -------------------------------------------------------------------------------- /docs/coding-guides/sass.md: -------------------------------------------------------------------------------- 1 | # Sass coding guide 2 | 3 | [Sass](http://sass-lang.com) is a superset of CSS, which brings a lot of developer candy to help scaling CSS in large 4 | projects and keeping it maintainable. 5 | 6 | The main benefits of using Sass over plain CSS are _variables_, _nesting_ and _mixins_, see the 7 | [basics guide](http://sass-lang.com/guide) for more details. 8 | 9 | > Note that this project use the newer, CSS-compatible **SCSS** syntax over the old 10 | > [indented syntax](http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html). 11 | 12 | ## Naming conventions 13 | 14 | - In the CSS world, everything should be named in `kebab-case` (lowercase words separated with a `-`). 15 | - File names should always be in `kebab-case` 16 | 17 | ## Coding rules 18 | 19 | - Use single quotes `'` for strings 20 | - Use this general nesting hierarchy when constructing your styles: 21 | 22 | ```scss 23 | // The base component class acts as the namespace, to avoid naming and style collisions 24 | .my-component { 25 | // Put here all component elements (flat) 26 | .my-element { 27 | // Use a third-level only for modifiers and state variations 28 | &.active { ... } 29 | } 30 | } 31 | ``` 32 | 33 | Note that with 34 | [Angular view encapsulation](https://angular.io/docs/ts/latest/guide/component-styles.html#!#view-encapsulation), 35 | the first "namespace" level of nesting is not necessary as Angular takes care of the scoping for avoid collisions. 36 | 37 | > As a side note, we are aware of the [BEM naming approach](https://en.bem.info/tools/bem/bem-naming/), but we found 38 | > it impractical for large projects. The nesting approach has drawbacks such as increased specificity, but it helps 39 | > keeping everything nicely organized, and more importantly, _scoped_. 40 | 41 | Also keep in mind this general rules: 42 | 43 | - Always use **class selectors**, never use ID selectors and avoid element selectors whenever possible 44 | - No more than **3 levels** of nesting 45 | - No more than **3 qualifiers** 46 | 47 | ## Best practices 48 | 49 | - Use object-oriented CSS (OOCSS): 50 | 51 | - Factorize common code in base class, and extend it, for example: 52 | 53 | ```scss 54 | // Base button class 55 | .btn { ... } 56 | 57 | // Color variation 58 | .btn-warning { ... } 59 | 60 | // Size variation 61 | .btn-small { ... } 62 | ``` 63 | 64 | - Try to name class by semantic, not style nor function for better reusability: 65 | Use `.btn-warning`, not `btn-orange` nor `btn-cancel` 66 | - Avoid undoing style, refactor using common base classes and extensions 67 | 68 | - Keep your style scoped 69 | 70 | - Clearly separate **global** (think _framework_) and **components** style 71 | - Global style should only go in `src/theme/`, never in components 72 | - Avoid style interactions between components, if some style may need to be shared, refactor it as a framework 73 | component in put it in your global theme. 74 | - Avoid using wider selectors than needed: always use classes if you can! 75 | 76 | - Avoid rules multiplication 77 | 78 | - The less CSS the better, factorize rules whenever it's possible 79 | - CSS is code, and like any code frequent refactoring is healthy 80 | 81 | - When ugly hacks cannot be avoided, create an explicit `src/hacks.scss` file and put it in: 82 | - These ugly hacks should only be **temporary** 83 | - Each hack should be documented with the author name, the problem and hack reason 84 | - Limit this file to a reasonable length (~100 lines) and refactor hacks with proper solutions when the limit is 85 | reached. 86 | 87 | ## Pitfalls 88 | 89 | - Never use the `!important` keyword. Ever. 90 | - Never use **inline** style in html, even _just for debugging_ (because we **KNOW** it will end up in your commit) 91 | 92 | ## Browser compatibility 93 | 94 | You should never use browser-specific prefixes in your code, as [autoprefixer](https://github.com/postcss/autoprefixer) 95 | takes care of that part for you during the build process. 96 | You just need to declare which browsers you target in the [`browserslist`](https://github.com/ai/browserslist) file. 97 | 98 | ## Enforcement 99 | 100 | Coding rules are enforced in this project with [stylelint](https://stylelint.io). 101 | This tool also checks the compatibility of the rules used against the browsers you are targeting (specified in the 102 | [`browserslist`](https://github.com/ai/browserslist) file), via [doiuse](https://github.com/anandthakker/doiuse). 103 | -------------------------------------------------------------------------------- /docs/coding-guides/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript coding guide 2 | 3 | [TypeScript](http://www.typescriptlang.org) is a superset of JavaScript that greatly helps building large web 4 | applications. 5 | 6 | Coding conventions and best practices comes from the 7 | [TypeScript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines), and are also detailed in the 8 | [TypeScript Deep Dive Style Guide](https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html). 9 | In addition, this project also follows the general [Angular style guide](https://angular.io/guide/styleguide). 10 | 11 | ## Naming conventions 12 | 13 | - Use `PascalCase` for types, classes, interfaces, constants and enum values. 14 | - Use `camelCase` for variables, properties and functions 15 | - Avoid prefixing interfaces with a capital `I`, see [Angular style guide](https://angular.io/guide/styleguide#!#03-03) 16 | - Do not use `_` as a prefix for private properties. An exception can be made for backing fields like this: 17 | ```typescript 18 | private _foo: string; 19 | get foo() { return this._foo; } // foo is read-only to consumers 20 | ``` 21 | 22 | ## Ordering 23 | 24 | - Within a file, type definitions should come first 25 | - Within a class, these priorities should be respected: 26 | - Properties comes before functions 27 | - Static symbols comes before instance symbols 28 | - Public symbols comes before private symbols 29 | 30 | ## Coding rules 31 | 32 | - Use single quotes `'` for strings 33 | - Always use strict equality checks: `===` and `!==` instead of `==` or `!=` to avoid comparison pitfalls (see 34 | [JavaScript equality table](https://dorey.github.io/JavaScript-Equality-Table/)). 35 | The only accepted usage for `==` is when you want to check a value against `null` or `undefined`. 36 | - Use `[]` instead of `Array` constructor 37 | - Use `{}` instead of `Object` constructor 38 | - Always specify types for function parameters and returns (if applicable) 39 | - Do not export types/functions unless you need to share it across multiple components 40 | - Do not introduce new types/values to the global namespace 41 | - Use arrow functions over anonymous function expressions 42 | - Only surround arrow function parameters when necessary. 43 | For example, `(x) => x + x` is wrong but the following are correct: 44 | - `x => x + x` 45 | - `(x, y) => x + y` 46 | - `(x: T, y: T) => x === y` 47 | 48 | ## Definitions 49 | 50 | In order to infer types from JavaScript modules, TypeScript language supports external type definitions. They are 51 | located in the `node_modules/@types` folder. 52 | 53 | To manage type definitions, use standard `npm install|update|remove` commands. 54 | 55 | ## Enforcement 56 | 57 | Coding rules are enforced in this project via [TSLint](https://github.com/palantir/tslint). 58 | Angular-specific rules are also enforced via the [Codelyzer](https://github.com/mgechev/codelyzer) rule extensions. 59 | 60 | ## Learn more 61 | 62 | The read of [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript) is recommended, this is a very good 63 | reference book for TypeScript (and also open-source). 64 | -------------------------------------------------------------------------------- /docs/coding-guides/unit-tests.md: -------------------------------------------------------------------------------- 1 | # Unit tests coding guide 2 | 3 | The main objective of unit tests is to detect regressions and to help you design software components. A suite of 4 | _good_ unit tests can be _immensely_ valuable for your project and makes it easier to refactor and expand your code. 5 | But keep in mind that a suite of _bad_ unit tests can also be _immensely_ painful, and hurt your development by 6 | inhibiting your ability to refactor or alter your code in any way. 7 | 8 | ## What to test? 9 | 10 | Everything! But if you need priorities, at least all business logic code must be tested: services, helpers, models... 11 | Shared directives/components should also be covered by unit tests, if you do not have the time to test every single 12 | component. 13 | 14 | Keep in mind that component unit tests should not overlap with [end-to-end tests](e2e-tests.md): while unit the tests 15 | cover the isolated behavior of the component bindings and methods, the end-to-end tests in opposition should cover the 16 | integration and interactions with other app components based on real use cases scenarios. 17 | 18 | ## Good practices 19 | 20 | - Name your tests cleanly and consistently 21 | - Do not only test nominal cases, the most important tests are the one covering the edge cases 22 | - Each test should be independent to all the others 23 | - Avoid unnecessary assertions: it's counter-productive to assert anything covered by another test, it just increase 24 | pointless failures and maintenance workload 25 | - Test only one code unit at a time: if you cannot do this, it means you have an architecture problem in your app 26 | - Mock out all external dependencies and state: if there is too much to mock, it is often a sign that maybe you 27 | should split your tested module into several more independent modules 28 | - Clearly separate or identify these 3 stages of each unit test (the _3A_): _arrange_, _act_ and _assert_ 29 | - When you fix a bug, add a test case for it to prevent regression 30 | 31 | ## Pitfalls 32 | 33 | - Sometimes your architecture might mean your code modify static variables during unit tests. Avoid this if you can, 34 | but if you can't, at least make sure each test resets the relevant statics before and after your tests. 35 | - Don’t unit-test configuration settings 36 | - Improving test coverage is good, but having meaningful tests is better: start with the latter first, and **only after 37 | essential features of your code unit are tested**, your can think of improving the coverage. 38 | 39 | ## Unit testing with Angular 40 | 41 | A good starting point for learning is the official 42 | [testing guide](https://angular.io/docs/ts/latest/guide/testing.html). 43 | 44 | But as you will most likely want to go bit further in real world apps, these 45 | [example test snippets](https://gist.github.com/wkwiatek/e8a4a9d92abc4739f04f5abddd3de8a7) are also very helpful to 46 | learn how to cover most common testing use cases. 47 | -------------------------------------------------------------------------------- /docs/corporate-proxy.md: -------------------------------------------------------------------------------- 1 | # Working behind a corporate proxy 2 | 3 | ## Environment 4 | 5 | Most tools (including npm and git) use the `HTTP_PROXY` and `HTTPS_PROXY` environment variables to work with a 6 | corporate proxy. 7 | 8 | ### Windows 9 | 10 | In Windows environments, add the `HTTP_PROXY` and `HTTPS_PROXY` system environment variable, with these values: 11 | 12 | - HTTP_PROXY: `http://:@:` 13 | - HTTPS_PROXY: `%HTTP_PROXY%` 14 | 15 | ### Unix 16 | 17 | Add these lines to your `~/.bash_profile` or `~/.profile`: 18 | 19 | ```sh 20 | export HTTP_PROXY="http://:@:" 21 | export HTTPS_PROXY="$HTTP_PROXY" 22 | ``` 23 | 24 | ## Proxy with SSL custom certificate 25 | 26 | Some proxy like **zscaler** use a custom SSL certificate to inspect request, which may cause npm commands to fail. 27 | 28 | To solve this problem, you can disable the `strict-ssl` option in npm. 29 | 30 | ## Proxy exceptions 31 | 32 | If you need to access repositories on your local network that should bypass proxy, set the `NO_PROXY` environment 33 | variable, in the same way as `HTTP_PROXY`: 34 | 35 | ### Windows 36 | 37 | - NO_PROXY: `127.0.0.1, localhost, ` 38 | 39 | ### Unix 40 | 41 | ```sh 42 | export NO_PROXY="127.0.0.1, localhost, " 43 | ``` 44 | 45 | ### Npm 46 | 47 | Run this command in your project directory: 48 | 49 | ```sh 50 | npm set strict-ssl false 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/i18n.md: -------------------------------------------------------------------------------- 1 | # I18n 2 | 3 | The internationalization of the application is managed by [ngx-translate](https://github.com/ngx-translate/core). 4 | 5 | ## Adding translatable strings 6 | 7 | ### In HTML templates 8 | 9 | Use the `translate` directive on an HTML element to automatically translate its content: 10 | 11 | ```html 12 | This text will be translated. 13 | ``` 14 | 15 | You can also use the `translate` pipe if needed: 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ### In TypeScript code 22 | 23 | If you need to translate strings in JavaScript code, import the `TranslateService` dependency and use the asynchronous 24 | `get()` method: 25 | 26 | ```typescript 27 | let title; 28 | translateService.get('My page title').subscribe((res: string) => { 29 | title = res; 30 | }); 31 | ``` 32 | 33 | ## Extracting strings to translate 34 | 35 | Once you are ready to translate your app, just run `npm run translations:extract`. 36 | It will create a `template.json` file in the `src/translations` folder. 37 | 38 | You can then use any text or code editor to generate the `.json` files for each of your supported languages, and put 39 | them in the `src/translations` folder. 40 | 41 | Do no forget to edit the files in `src/environment` to add the supported languages of your application. 42 | 43 | ### Marking strings for extraction 44 | 45 | If strings are not directly passed to `translateService` or put in HTML templates, they may be missing from the 46 | extraction process. 47 | 48 | For these cases, you have to use the dummy `extract()` function: 49 | 50 | ```typescript 51 | import { extract } from './core/i18n.service'; 52 | 53 | function toBeTranslatedLater() { 54 | return extract('A string to be translated'); 55 | } 56 | ``` 57 | 58 | Strings marked like this will then be properly extracted. 59 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # freelancer-theme 2 | 3 | Welcome to the project documentation! 4 | 5 | Use `npm run docs` for easier navigation. 6 | 7 | ## Available documentation 8 | 9 | [[index]] 10 | -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | # Browser routing 2 | 3 | To allow navigation without triggering a server request, Angular now use by default the 4 | [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) 5 | API enabling natural URL style (like `localhost:4200/home/`), in opposition to Angular 1 which used the _hashbang_ hack 6 | routing style (like `localhost:4200/#/home/`). 7 | 8 | This change has several consequences you should know of, be sure to read the 9 | [browser URL styles](https://angular.io/docs/ts/latest/guide/router.html#!#browser-url-styles) notice to fully 10 | understand the differences between the two approaches. 11 | 12 | In short: 13 | 14 | - It is only supported on modern browsers (IE10+), a [polyfill](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#html5-history-api-pushstate-replacestate-popstate) 15 | is required for older browsers. 16 | 17 | - You have the option to perform _server-side rendering_ later if you need to increase your app perceived performance. 18 | 19 | - You need to [configure URL rewriting](#server-configuration) on your server so that all routes serve your index file. 20 | 21 | It is still possible to revert to the hash strategy, but unless you have specific needs, you should stick with the 22 | default HTML5 routing mode. 23 | 24 | ## Server configuration 25 | 26 | To allow your angular application working properly as a _Single Page Application_ (SPA) and allow bookmarking or 27 | refreshing any page, you need some configuration on your server, otherwise you will be running into troubles. 28 | 29 | > Note that during development, the live reload server already supports SPA mode. 30 | 31 | The basic idea is simply to serve the `index.html` file for every request aimed at your application. 32 | 33 | Here is an example on how to perform this on an [Express](http://expressjs.com) NodeJS server: 34 | 35 | ```js 36 | // Put this in your `server.js` file, after your other rules (APIs, static files...) 37 | app.get('/*', function(req, res) { 38 | res.sendFile(__dirname + '/index.html'); 39 | }); 40 | ``` 41 | 42 | For other servers like [Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/) or 43 | [Apache](http://httpd.apache.org/docs/2.0/misc/rewriteguide.html), you may look for how to perform _URL rewriting_. 44 | -------------------------------------------------------------------------------- /docs/updating.md: -------------------------------------------------------------------------------- 1 | # Updating npm dependencies 2 | 3 | - Check outdated packages 4 | 5 | ```sh 6 | npm outdated 7 | ``` 8 | 9 | - Update local packages according to `package.json` 10 | 11 | ```sh 12 | npm update 13 | ``` 14 | 15 | - Upgrade packages manually 16 | 17 | ```sh 18 | npm install --save[-dev] @latest 19 | ``` 20 | 21 | Alternatively, you can use [npm-check](https://github.com/dylang/npm-check) to perform an interactive upgrade: 22 | 23 | ```sh 24 | npm-check -u --skip-unused 25 | ``` 26 | 27 | ## Locking package versions 28 | 29 | Starting from `npm@5` a new `package-lock.json` file is 30 | [automatically generated](https://docs.npmjs.com/files/package-locks) when using `npm install` commands, to ensure a 31 | reproducible dependency tree and avoid unwanted package updates. 32 | 33 | If you use a previous npm version, it is recommended to use [npm shrinkwrap](https://docs.npmjs.com/cli/shrinkwrap) to 34 | lock down all your dependencies version: 35 | 36 | ```sh 37 | npm shrinkwrap --dev 38 | ``` 39 | 40 | This will create a file `npm-shrinkwrap.json` alongside your `package.json` files. 41 | 42 | > Do not forget to run shrinkwrap each time you manually update your dependencies! 43 | 44 | # Updating angular-related dependencies 45 | 46 | See the [Angular update website](https://update.angular.io) to guide you through the updating/upgrading steps. 47 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: ['./src/**/*.e2e-spec.ts'], 9 | capabilities: { 10 | browserName: process.env.PROTRACTOR_BROWSER || 'chrome' 11 | }, 12 | // Only works with Chrome and Firefox 13 | directConnect: true, 14 | baseUrl: 'http://localhost:4200/', 15 | framework: 'jasmine2', 16 | jasmineNodeOpts: { 17 | showColors: true, 18 | defaultTimeoutInterval: 30000, 19 | print: function() {} 20 | }, 21 | onPrepare() { 22 | require('ts-node').register({ 23 | project: require('path').join(__dirname, './tsconfig.e2e.json') 24 | }); 25 | // Better console spec reporter 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('app', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display hello message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Hello world !'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Use the Page Object pattern to define the page under test. 3 | * See docs/coding-guide/e2e-tests.md for more info. 4 | */ 5 | 6 | import { browser, element, by } from 'protractor'; 7 | 8 | export class AppPage { 9 | constructor() { 10 | // Forces default language 11 | this.navigateTo(); 12 | if (localStorage) { 13 | browser.executeScript(() => localStorage.setItem('language', 'en-US')); 14 | } 15 | } 16 | 17 | navigateTo() { 18 | return browser.get('/'); 19 | } 20 | 21 | getParagraphText() { 22 | return element(by.css('app-root mat-card-title')).getText(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["jasmine", "jasminewd2", "node"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [ 4 | { 5 | "name": "app", 6 | "installMode": "prefetch", 7 | "resources": { 8 | "files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"] 9 | } 10 | }, 11 | { 12 | "name": "assets", 13 | "installMode": "lazy", 14 | "updateMode": "prefetch", 15 | "resources": { 16 | "files": ["/assets/**"] 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-material-extension/freelancer-theme", 3 | "version": "0.0.0-development", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "build": "npm run env -s && ng build --prod", 8 | "build:doc": "cd ../ && gulp build:doc", 9 | "build:prod": "ng build --prod ", 10 | "build:ci": "ng build --prod --build-optimizer --base-href /freelancer-theme/ --deploy-url /freelancer-theme/", 11 | "build:client-and-server-bundles": "npm run env -s && ng build --prod && ng run freelancer-theme:server:production", 12 | "build:client-and-server-bundles:ci": "npm run env -s && npm run build:ci && ng run freelancer-theme:server:production", 13 | "build:prerender": "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:prerender", 14 | "build:prerender:ci": "npm run build:client-and-server-bundles:ci && npm run webpack:server && npm run generate:prerender", 15 | "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server", 16 | "clean": "rm -rf dist", 17 | "codecov": "codecov", 18 | "compile:server": "tsc -p server.tsconfig.json", 19 | "deploy:demo": "ngh --dir dist/browser/ --message=\"chore(demo): deploy new version\"", 20 | "lint": "ng lint && stylelint \"src/**/*.scss\" --syntax scss && htmlhint \"src\" --config .htmlhintrc", 21 | "test": "npm run env -s && ng test", 22 | "test:ci": "npm run env -s && ng test --code-coverage --watch=false", 23 | "e2e": "npm run env -s && ng e2e", 24 | "translations:extract": "ngx-translate-extract --input ./src --output ./src/translations/template.json --format=json --clean -sort --marker extract", 25 | "docs": "hads ./docs -o", 26 | "env": "ngx-scripts env npm_package_version", 27 | "generate:prerender": "cd dist && node prerender", 28 | "prettier": "prettier --write \"./{src,e2e}/**/*.{ts,js,scss}\"", 29 | "prettier:check": "prettier --list-different \"./{src,e2e}/**/*.{ts,js,scss}\"", 30 | "postinstall": "npm run prettier -s", 31 | "precommit": "pretty-quick --staged", 32 | "generate": "ng generate", 33 | "semantic-release": "semantic-release", 34 | "start": "npm run env -s && ng serve --aot --proxy-config proxy.conf.js --port 4210", 35 | "serve:prod": "ng serve --prod", 36 | "serve:prerender": "cd dist/browser && http-server", 37 | "serve:ssr": "node dist/server", 38 | "serve:sw": "npm run build -s && npx http-server ./dist -p 4200", 39 | "webpack:server": "webpack --config webpack.server.config.js --progress --colors" 40 | }, 41 | "dependencies": { 42 | "@angular/animations": "^7.2.8", 43 | "@angular/cdk": "^7.3.4", 44 | "@angular/common": "^7.2.8", 45 | "@angular/compiler": "^7.2.8", 46 | "@angular/core": "^7.2.8", 47 | "@angular/flex-layout": "7.0.0-beta.24", 48 | "@angular/forms": "^7.2.8", 49 | "@angular/http": "^7.2.8", 50 | "@angular/material": "^7.3.4", 51 | "@angular/platform-browser": "^7.2.8", 52 | "@angular/platform-browser-dynamic": "^7.2.8", 53 | "@angular/platform-server": "^7.2.8", 54 | "@angular/router": "^7.2.8", 55 | "@angular/service-worker": "^7.2.8", 56 | "@nguniversal/express-engine": "^7.1.1", 57 | "@nguniversal/module-map-ngfactory-loader": "^7.1.1", 58 | "@ngx-translate/core": "^11.0.1", 59 | "angulartics2": "^7.4.1", 60 | "core-js": "^2.6.9", 61 | "hammerjs": "^2.0.8", 62 | "lodash": "^4.17.14", 63 | "material-design-icons": "^3.0.1", 64 | "material-design-icons-iconfont": "^5.0.1", 65 | "ngx-webstorage": "^3.0.2", 66 | "rxjs": "^6.4.0", 67 | "ts-loader": "^5.3.3", 68 | "tslib": "^1.10.0", 69 | "zone.js": "^0.8.29" 70 | }, 71 | "devDependencies": { 72 | "@angular-devkit/build-angular": "^0.13.5", 73 | "@angular/cli": "^7.3.5", 74 | "@angular/compiler-cli": "^7.2.8", 75 | "@angular/language-service": "^7.2.8", 76 | "@biesbjerg/ngx-translate-extract": "^2.3.4", 77 | "@ngx-rocket/scripts": "^3.0.4", 78 | "@semantic-release/changelog": "^3.0.4", 79 | "@semantic-release/commit-analyzer": "^6.2.0", 80 | "@semantic-release/git": "^7.0.16", 81 | "@semantic-release/release-notes-generator": "^7.2.1", 82 | "@types/express": "^4.17.0", 83 | "@types/jasmine": "^3.3.13", 84 | "@types/jasminewd2": "^2.0.2", 85 | "@types/lodash": "^4.14.136", 86 | "@types/node": "^10.14.12", 87 | "angular-cli-ghpages": "^0.5.3", 88 | "codecov": "^3.2.0", 89 | "codelyzer": "^4.4.4", 90 | "express": "^4.17.1", 91 | "hads": "^1.7.2", 92 | "htmlhint": "^0.10.3", 93 | "http-server": "^0.11.1", 94 | "https-proxy-agent": "^2.2.2", 95 | "husky": "^1.3.1", 96 | "jasmine-core": "~3.3.0", 97 | "jasmine-spec-reporter": "^4.1.0", 98 | "karma": "^4.2.0", 99 | "karma-chrome-launcher": "^2.2.0", 100 | "karma-cli": "~2.0.0", 101 | "karma-coverage-istanbul-reporter": "^2.0.5", 102 | "karma-jasmine": "^2.0.1", 103 | "karma-junit-reporter": "^1.2.0", 104 | "prettier": "^1.18.2", 105 | "pretty-quick": "^1.11.1", 106 | "protractor": "~5.4.0", 107 | "puppeteer": "^1.18.1", 108 | "reflect-metadata": "^0.1.13", 109 | "semantic-release": "^15.13.18", 110 | "stylelint": "~9.10.1", 111 | "stylelint-config-prettier": "^4.0.0", 112 | "stylelint-config-recommended-scss": "~3.2.0", 113 | "stylelint-config-standard": "~18.2.0", 114 | "stylelint-scss": "^3.9.1", 115 | "ts-node": "^8.3.0", 116 | "tslint": "~5.12.1", 117 | "tslint-config-prettier": "^1.18.0", 118 | "typescript": "~3.2.4", 119 | "webpack-cli": "^3.3.5" 120 | }, 121 | "jest": { 122 | "preset": "jest-preset-angular", 123 | "coverageReporters": [ 124 | "lcov", 125 | "text" 126 | ], 127 | "setupTestFrameworkScriptFile": "/src/setupJest.ts" 128 | }, 129 | "prettier": { 130 | "singleQuote": true, 131 | "overrides": [ 132 | { 133 | "files": "*.scss", 134 | "options": { 135 | "singleQuote": false 136 | } 137 | } 138 | ] 139 | }, 140 | "repository": { 141 | "type": "git", 142 | "url": "https://github.com/angular-material-extensions/freelancer-theme.git" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /prerender.ts: -------------------------------------------------------------------------------- 1 | // Load zone.js for the server. 2 | import 'zone.js/dist/zone-node'; 3 | import 'reflect-metadata'; 4 | import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; 5 | import { join } from 'path'; 6 | 7 | import { enableProdMode } from '@angular/core'; 8 | // Faster server renders w/ Prod mode (dev mode never needed) 9 | enableProdMode(); 10 | 11 | // Import module map for lazy loading 12 | import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; 13 | import { renderModuleFactory } from '@angular/platform-server'; 14 | import { ROUTES } from './static.paths'; 15 | 16 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 17 | const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main'); 18 | 19 | const BROWSER_FOLDER = join(process.cwd(), 'browser'); 20 | 21 | // Load the index.html file containing referances to your application bundle. 22 | const index = readFileSync(join(BROWSER_FOLDER, 'index.html'), 'utf8'); 23 | 24 | let previousRender = Promise.resolve(); 25 | 26 | // Iterate each route path 27 | ROUTES.forEach(route => { 28 | const fullPath = join(BROWSER_FOLDER, route); 29 | 30 | // Make sure the directory structure is there 31 | if (!existsSync(fullPath)) { 32 | mkdirSync(fullPath); 33 | } 34 | 35 | // Writes rendered HTML to index.html, replacing the file if it already exists. 36 | previousRender = previousRender 37 | .then(_ => 38 | renderModuleFactory(AppServerModuleNgFactory, { 39 | document: index, 40 | url: route, 41 | extraProviders: [provideModuleMap(LAZY_MODULE_MAP)] 42 | }) 43 | ) 44 | .then(html => writeFileSync(join(fullPath, 'index.html'), html)); 45 | }); 46 | -------------------------------------------------------------------------------- /proxy.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HttpsProxyAgent = require('https-proxy-agent'); 4 | 5 | /* 6 | * API proxy configuration. 7 | * This allows you to proxy HTTP request like `http.get('/api/stuff')` to another server/port. 8 | * This is especially useful during app development to avoid CORS issues while running a local server. 9 | * For more details and options, see https://github.com/angular/angular-cli#proxy-to-backend 10 | */ 11 | const proxyConfig = [ 12 | { 13 | context: '/api', 14 | pathRewrite: { '^/api': '' }, 15 | target: 'https://api.chucknorris.io', 16 | changeOrigin: true, 17 | secure: false 18 | } 19 | ]; 20 | 21 | /* 22 | * Configures a corporate proxy agent for the API proxy if needed. 23 | */ 24 | function setupForCorporateProxy(proxyConfig) { 25 | if (!Array.isArray(proxyConfig)) { 26 | proxyConfig = [proxyConfig]; 27 | } 28 | 29 | const proxyServer = process.env.http_proxy || process.env.HTTP_PROXY; 30 | let agent = null; 31 | 32 | if (proxyServer) { 33 | console.log(`Using corporate proxy server: ${proxyServer}`); 34 | agent = new HttpsProxyAgent(proxyServer); 35 | proxyConfig.forEach(entry => { 36 | entry.agent = agent; 37 | }); 38 | } 39 | 40 | return proxyConfig; 41 | } 42 | 43 | module.exports = setupForCorporateProxy(proxyConfig); 44 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import 'reflect-metadata'; 3 | import { enableProdMode } from '@angular/core'; 4 | // Express Engine 5 | import { ngExpressEngine } from '@nguniversal/express-engine'; 6 | // Import module map for lazy loading 7 | import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; 8 | 9 | import * as express from 'express'; 10 | import { join } from 'path'; 11 | 12 | // Faster server renders w/ Prod mode (dev mode never needed) 13 | enableProdMode(); 14 | 15 | // Express server 16 | const app = express(); 17 | 18 | const PORT = process.env.PORT || 4000; 19 | const DIST_FOLDER = join(process.cwd(), 'dist'); 20 | 21 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 22 | const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main'); 23 | 24 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 25 | app.engine( 26 | 'html', 27 | ngExpressEngine({ 28 | bootstrap: AppServerModuleNgFactory, 29 | providers: [provideModuleMap(LAZY_MODULE_MAP)] 30 | }) 31 | ); 32 | 33 | app.set('view engine', 'html'); 34 | app.set('views', join(DIST_FOLDER, 'browser')); 35 | 36 | // Example Express Rest API endpoints 37 | // app.get('/api/**', (req, res) => { }); 38 | // Server static files from /browser 39 | app.get( 40 | '*.*', 41 | express.static(join(DIST_FOLDER, 'browser'), { 42 | maxAge: '1y' 43 | }) 44 | ); 45 | 46 | // All regular routes use the Universal engine 47 | app.get('*', (req: any, res: any) => { 48 | res.render('index', { req }); 49 | }); 50 | 51 | // Start up the Node server 52 | app.listen(PORT, () => { 53 | console.log(`Node Express server listening on http://localhost:${PORT}`); 54 | }); 55 | -------------------------------------------------------------------------------- /src/app/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { extract } from '@app/core'; 5 | import { AboutComponent } from './about.component'; 6 | 7 | const routes: Routes = [ 8 | // Module is lazy loaded, see app-routing.module.ts 9 | { path: '', component: AboutComponent, data: { title: extract('About') } } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | providers: [] 16 | }) 17 | export class AboutRoutingModule {} 18 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | APP_NAME 5 |

6 | 7 | code 8 | Version {{ version }} 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/about/about.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | padding: 1rem; 4 | } 5 | 6 | .mat-icon { 7 | vertical-align: middle; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | import { MaterialModule } from '@app/material.module'; 6 | import { AboutComponent } from './about.component'; 7 | 8 | describe('AboutComponent', () => { 9 | let component: AboutComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [BrowserAnimationsModule, FlexLayoutModule, MaterialModule], 15 | declarations: [AboutComponent] 16 | }).compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(AboutComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { environment } from '@env/environment'; 4 | 5 | @Component({ 6 | selector: 'app-about', 7 | templateUrl: './about.component.html', 8 | styleUrls: ['./about.component.scss'] 9 | }) 10 | export class AboutComponent implements OnInit { 11 | version: string = environment.version; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | 6 | import { MaterialModule } from '@app/material.module'; 7 | import { AboutRoutingModule } from './about-routing.module'; 8 | import { AboutComponent } from './about.component'; 9 | 10 | @NgModule({ 11 | imports: [CommonModule, TranslateModule, FlexLayoutModule, MaterialModule, AboutRoutingModule], 12 | declarations: [AboutComponent] 13 | }) 14 | export class AboutModule {} 15 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | // Shell.childRoutes([{ path: 'about', loadChildren: 'app/about/about.module#AboutModule' }]), 6 | // Fallback when no prior route is matched 7 | // no routes 8 | { path: '**', redirectTo: '', pathMatch: 'full' } 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], 13 | exports: [RouterModule], 14 | providers: [] 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { Angulartics2Module } from 'angulartics2'; 5 | 6 | import { CoreModule } from '@app/core'; 7 | import { AppComponent } from './app.component'; 8 | 9 | describe('AppComponent', () => { 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [Angulartics2Module.forRoot(), RouterTestingModule, TranslateModule.forRoot(), CoreModule], 13 | declarations: [AppComponent], 14 | providers: [] 15 | }); 16 | TestBed.compileComponents(); 17 | })); 18 | 19 | it('should create the app', async(() => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app).toBeTruthy(); 23 | })); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core'; 2 | import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; 3 | import { DomSanitizer, Title } from '@angular/platform-browser'; 4 | import { TranslateService } from '@ngx-translate/core'; 5 | import { merge } from 'rxjs'; 6 | import { filter, map, mergeMap } from 'rxjs/operators'; 7 | import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; 8 | 9 | import { environment } from '@env/environment'; 10 | import { I18nService, Logger } from '@app/core'; 11 | import { MatIconRegistry } from '@angular/material'; 12 | import { isPlatformServer } from '@angular/common'; 13 | 14 | const log = new Logger('App'); 15 | 16 | @Component({ 17 | selector: 'app-root', 18 | templateUrl: './app.component.html', 19 | styleUrls: ['./app.component.scss'] 20 | }) 21 | export class AppComponent implements OnInit { 22 | constructor( 23 | @Inject(PLATFORM_ID) private platformId: string, 24 | private router: Router, 25 | private activatedRoute: ActivatedRoute, 26 | private titleService: Title, 27 | private translateService: TranslateService, 28 | // do not remove the analytics injection, even if the call in ngOnInit() is removed 29 | // this injection initializes page tracking through the router 30 | private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, 31 | private i18nService: I18nService, 32 | private _iconRegistry: MatIconRegistry, 33 | private _sanitizer: DomSanitizer 34 | ) { 35 | // domain and port for SSR in this example is static. Use i.e. environment 36 | // files to use appropriate dev/prod domain:port 37 | // const domain = (isPlatformServer(platformId)) 38 | // ? 'http://localhost:4000' : 'https://angular-material-extensions.github.io/freelancer-theme'; 39 | const domain = isPlatformServer(platformId) ? 'http://localhost:4000' : ''; 40 | console.log('on construct app component'); 41 | _iconRegistry 42 | .addSvgIcon('facebook', _sanitizer.bypassSecurityTrustResourceUrl(`${domain}/assets/icons/facebook.svg`)) 43 | .addSvgIcon('twitter', _sanitizer.bypassSecurityTrustResourceUrl(`${domain}/assets/icons/twitter.svg`)) 44 | .addSvgIcon('linkedin', _sanitizer.bypassSecurityTrustResourceUrl(`${domain}/assets/icons/linkedin.svg`)) 45 | .addSvgIcon('github', _sanitizer.bypassSecurityTrustResourceUrl(`${domain}/assets/icons/github-circle.svg`)); 46 | } 47 | 48 | ngOnInit() { 49 | // Setup logger 50 | if (environment.production) { 51 | Logger.enableProductionMode(); 52 | } 53 | 54 | log.debug('init'); 55 | 56 | this.angulartics2GoogleAnalytics.eventTrack(environment.version, { category: 'App initialized' }); 57 | 58 | // Setup translations 59 | this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages); 60 | 61 | const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd)); 62 | 63 | // Change page title on navigation or language change, based on route data 64 | merge(this.translateService.onLangChange, onNavigationEnd) 65 | .pipe( 66 | map(() => { 67 | let route = this.activatedRoute; 68 | while (route.firstChild) { 69 | route = route.firstChild; 70 | } 71 | return route; 72 | }), 73 | filter(route => route.outlet === 'primary'), 74 | mergeMap(route => route.data) 75 | ) 76 | .subscribe(event => { 77 | const title = event['title']; 78 | if (title) { 79 | this.titleService.setTitle(this.translateService.instant(title)); 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { ServiceWorkerModule } from '@angular/service-worker'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | import { MaterialModule } from './material.module'; 9 | import { Angulartics2Module } from 'angulartics2'; 10 | import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; 11 | 12 | import { environment } from '@env/environment'; 13 | import { CoreModule } from '@app/core'; 14 | import { SharedModule } from '@app/shared'; 15 | import { HomeModule } from './components/home/home.module'; 16 | import { ShellModule } from './components/shell/shell.module'; 17 | import { AppComponent } from './app.component'; 18 | import { AppRoutingModule } from './app-routing.module'; 19 | import { ProjectDialogComponent } from '@app/components/project-dialog/project-dialog.component'; 20 | import { NgxWebstorageModule } from 'ngx-webstorage'; 21 | 22 | @NgModule({ 23 | imports: [ 24 | BrowserModule.withServerTransition({ appId: 'freelancer-theme' }), 25 | ServiceWorkerModule.register('./ngsw-worker.js', { enabled: environment.production }), 26 | NgxWebstorageModule.forRoot(), 27 | Angulartics2Module.forRoot(), 28 | TranslateModule.forRoot(), 29 | SharedModule.forRoot(), 30 | FormsModule, 31 | HttpClientModule, 32 | BrowserAnimationsModule, 33 | MaterialModule, 34 | CoreModule, 35 | ShellModule, 36 | HomeModule, 37 | AppRoutingModule // must be imported as the last module as it contains the fallback route. 38 | ], 39 | declarations: [AppComponent, ProjectDialogComponent], 40 | entryComponents: [ProjectDialogComponent], 41 | providers: [], 42 | bootstrap: [AppComponent] 43 | }) 44 | export class AppModule { 45 | constructor(angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics) { 46 | angulartics2GoogleAnalytics.startTracking(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; 4 | 5 | import { AppModule } from './app.module'; 6 | import { AppComponent } from './app.component'; 7 | import { FlexLayoutServerModule } from '@angular/flex-layout/server'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | // The AppServerModule should import your AppModule followed 12 | // by the ServerModule from @angular/platform-server. 13 | AppModule, 14 | ServerModule, 15 | FlexLayoutServerModule, 16 | ModuleMapLoaderModule // <-- *Important* to have lazy-loaded routes work 17 | ], 18 | // Since the bootstrapped component is not inherited from your 19 | // imported AppModule, it needs to be repeated here. 20 | bootstrap: [AppComponent] 21 | }) 22 | export class AppServerModule {} 23 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |
7 |

About

8 |
9 |
10 | star 11 |
12 |
13 |
14 |
15 |
16 |

17 | {{ config?.about?.section1 }} 18 |

19 |
20 |
21 |

22 | {{ config?.about?.section2 }} 23 |

24 |
25 |
26 |
27 | 32 |
33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | padding: 1rem; 4 | } 5 | 6 | .mat-icon { 7 | vertical-align: middle; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | import { MaterialModule } from '../../material.module'; 6 | import { AboutComponent } from './about.component'; 7 | import { ConfigToken, DEFAULT_CONFIG } from '../../../config'; 8 | import { TranslateModule, TranslatePipe, TranslateService } from '@ngx-translate/core'; 9 | 10 | describe('AboutComponent', () => { 11 | let component: AboutComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [BrowserAnimationsModule, TranslateModule.forRoot(), FlexLayoutModule, MaterialModule], 17 | declarations: [AboutComponent], 18 | providers: [ 19 | TranslateService, 20 | { 21 | provide: ConfigToken, 22 | useValue: DEFAULT_CONFIG 23 | } 24 | ] 25 | }).compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(AboutComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | 3 | import { environment } from '../../../environments/environment'; 4 | import { Config, ConfigToken } from '../../../config'; 5 | 6 | @Component({ 7 | selector: 'app-about', 8 | templateUrl: './about.component.html', 9 | styleUrls: ['./about.component.scss'] 10 | }) 11 | export class AboutComponent implements OnInit { 12 | version: string = environment.version; 13 | 14 | constructor( 15 | @Inject(ConfigToken) 16 | public config: Config 17 | ) {} 18 | 19 | ngOnInit() {} 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { extract } from '../../core'; 5 | import { HomeComponent } from './home.component'; 6 | import { Shell } from '../shell/shell.service'; 7 | 8 | const routes: Routes = [ 9 | Shell.childRoutes([ 10 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 11 | { path: 'home', component: HomeComponent, data: { title: extract('Home') } } 12 | ]) 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forChild(routes)], 17 | exports: [RouterModule], 18 | providers: [] 19 | }) 20 | export class HomeRoutingModule {} 21 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | padding: 1em; 4 | } 5 | 6 | .logo { 7 | width: 150px; 8 | margin: 0 auto; 9 | } 10 | 11 | q { 12 | font-style: italic; 13 | quotes: "« " " »"; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { Angulartics2Module } from 'angulartics2'; 7 | 8 | import { CoreModule } from '../../core'; 9 | import { SharedModule } from '../../shared'; 10 | import { MaterialModule } from '../../material.module'; 11 | import { HomeComponent } from './home.component'; 12 | import { QuoteService } from './quote.service'; 13 | import { ConfigToken, DEFAULT_CONFIG } from '../../../config'; 14 | import { AboutComponent } from '@app/components/about/about.component'; 15 | import { TranslateModule } from '@ngx-translate/core'; 16 | 17 | describe('HomeComponent', () => { 18 | let component: HomeComponent; 19 | let fixture: ComponentFixture; 20 | 21 | beforeEach(async(() => { 22 | TestBed.configureTestingModule({ 23 | imports: [ 24 | BrowserAnimationsModule, 25 | TranslateModule.forRoot(), 26 | Angulartics2Module.forRoot(), 27 | FlexLayoutModule, 28 | MaterialModule, 29 | RouterTestingModule, 30 | CoreModule, 31 | SharedModule, 32 | HttpClientTestingModule 33 | ], 34 | declarations: [HomeComponent, AboutComponent], 35 | providers: [ 36 | QuoteService, 37 | { 38 | provide: ConfigToken, 39 | useValue: DEFAULT_CONFIG 40 | } 41 | ] 42 | }).compileComponents(); 43 | })); 44 | 45 | beforeEach(() => { 46 | fixture = TestBed.createComponent(HomeComponent); 47 | component = fixture.componentInstance; 48 | fixture.detectChanges(); 49 | }); 50 | 51 | it('should create', () => { 52 | expect(component).toBeTruthy(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { Config, ConfigToken } from '../../../config'; 3 | import { Project } from '../../../config.interface'; 4 | import { ProjectDialogComponent } from '@app/components/project-dialog/project-dialog.component'; 5 | import { MatDialog } from '@angular/material'; 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.scss'] 11 | }) 12 | export class HomeComponent { 13 | isLoading: boolean; 14 | socialEntries: any[] = []; 15 | 16 | constructor( 17 | @Inject(ConfigToken) 18 | public config: Config, 19 | public dialog: MatDialog 20 | ) { 21 | if (this.config.social) { 22 | this.socialEntries = Object.entries(this.config.social); 23 | console.log(this.socialEntries); 24 | } 25 | } 26 | 27 | openProjectDialog(project: Project) { 28 | this.dialog.open(ProjectDialogComponent, { 29 | data: project 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { Angulartics2Module } from 'angulartics2'; 6 | 7 | import { CoreModule } from '../../core'; 8 | import { SharedModule } from '../../shared'; 9 | import { MaterialModule } from '../../material.module'; 10 | import { HomeRoutingModule } from './home-routing.module'; 11 | import { HomeComponent } from './home.component'; 12 | import { QuoteService } from './quote.service'; 13 | import { AboutComponent } from '../about/about.component'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | CommonModule, 18 | TranslateModule, 19 | CoreModule, 20 | SharedModule, 21 | FlexLayoutModule, 22 | MaterialModule, 23 | Angulartics2Module, 24 | HomeRoutingModule 25 | ], 26 | declarations: [HomeComponent, AboutComponent], 27 | providers: [QuoteService] 28 | }) 29 | export class HomeModule {} 30 | -------------------------------------------------------------------------------- /src/app/components/home/quote.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject, async } from '@angular/core/testing'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | 4 | import { CoreModule, HttpCacheService } from '../../core'; 5 | import { QuoteService } from './quote.service'; 6 | 7 | describe('QuoteService', () => { 8 | let quoteService: QuoteService; 9 | let httpMock: HttpTestingController; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [CoreModule, HttpClientTestingModule], 14 | providers: [HttpCacheService, QuoteService] 15 | }); 16 | })); 17 | 18 | beforeEach(inject( 19 | [HttpCacheService, QuoteService, HttpTestingController], 20 | (htttpCacheService: HttpCacheService, _quoteService: QuoteService, _httpMock: HttpTestingController) => { 21 | quoteService = _quoteService; 22 | httpMock = _httpMock; 23 | 24 | htttpCacheService.cleanCache(); 25 | } 26 | )); 27 | 28 | afterEach(() => { 29 | httpMock.verify(); 30 | }); 31 | 32 | describe('getRandomQuote', () => { 33 | it('should return a random Chuck Norris quote', () => { 34 | // Arrange 35 | const mockQuote = { value: 'a random quote' }; 36 | 37 | // Act 38 | const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); 39 | 40 | // Assert 41 | randomQuoteSubscription.subscribe((quote: string) => { 42 | expect(quote).toEqual(mockQuote.value); 43 | }); 44 | httpMock.expectOne({}).flush(mockQuote); 45 | }); 46 | 47 | it('should return a string in case of error', () => { 48 | // Act 49 | const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); 50 | 51 | // Assert 52 | randomQuoteSubscription.subscribe((quote: string) => { 53 | expect(typeof quote).toEqual('string'); 54 | expect(quote).toContain('Error'); 55 | }); 56 | httpMock.expectOne({}).flush(null, { 57 | status: 500, 58 | statusText: 'error' 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/components/home/quote.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | import { map, catchError } from 'rxjs/operators'; 5 | 6 | const routes = { 7 | quote: (c: RandomQuoteContext) => `/jokes/random?category=${c.category}` 8 | }; 9 | 10 | export interface RandomQuoteContext { 11 | // The quote's category: 'dev', 'explicit'... 12 | category: string; 13 | } 14 | 15 | @Injectable() 16 | export class QuoteService { 17 | constructor(private httpClient: HttpClient) {} 18 | 19 | getRandomQuote(context: RandomQuoteContext): Observable { 20 | return this.httpClient 21 | .cache() 22 | .get(routes.quote(context)) 23 | .pipe( 24 | map((body: any) => body.value), 25 | catchError(() => of('Error, could not load joke :-(')) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/project-dialog/project-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ data.name }}

3 | 4 | 7 |
8 | 9 | 10 |
15 | 16 |

{{ data.description }}

17 |
18 |
19 | 20 | 21 | Visit 22 | 23 | -------------------------------------------------------------------------------- /src/app/components/project-dialog/project-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/app/components/project-dialog/project-dialog.component.scss -------------------------------------------------------------------------------- /src/app/components/project-dialog/project-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectDialogComponent } from './project-dialog.component'; 4 | import { MaterialModule } from '@app/material.module'; 5 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; 6 | 7 | describe('ProjectDialogComponent', () => { 8 | let component: ProjectDialogComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [MaterialModule], 14 | declarations: [ProjectDialogComponent], 15 | providers: [ 16 | { 17 | provide: MatDialogRef, 18 | useValue: {} 19 | }, 20 | { 21 | provide: MAT_DIALOG_DATA, 22 | useValue: {} // Add any data you wish to test if it is passed/used correctly 23 | } 24 | ] 25 | }).compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(ProjectDialogComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/components/project-dialog/project-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { Project } from '../../../config.interface'; 3 | import { MAT_DIALOG_DATA } from '@angular/material'; 4 | 5 | @Component({ 6 | selector: 'app-project-dialog', 7 | templateUrl: './project-dialog.component.html', 8 | styleUrls: ['./project-dialog.component.scss'] 9 | }) 10 | export class ProjectDialogComponent { 11 | constructor(@Inject(MAT_DIALOG_DATA) public data: Project) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/shell/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 7 | {{ config.title }} 8 |
9 |
10 | {{ config.title }} 11 |
12 | 13 |
14 | 15 | 18 | 21 | 24 |
25 | 28 | 29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 | 37 |
38 |
39 | 45 |

{{ config.title }}

46 |
47 |
48 | star 49 |
50 |
51 |

{{ config.description }}

52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /src/app/components/shell/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | 3 | .navbar { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | z-index: 1; 9 | } 10 | 11 | .brand { 12 | color: mat-color($app-primary, "default-contrast"); 13 | text-decoration: none; 14 | padding-right: 1rem; 15 | } 16 | 17 | .menu-button { 18 | margin-right: 1rem; 19 | } 20 | 21 | header .masthead { 22 | padding-top: calc(1rem + 72px); 23 | padding-bottom: 3rem; 24 | } 25 | 26 | header .masthead h1 { 27 | font-size: 3rem; 28 | line-height: 3rem; 29 | } 30 | 31 | header .masthead h2 { 32 | font-size: 1.3rem; 33 | //font-family: Lato; 34 | } 35 | 36 | @media (min-width: 992px) { 37 | header .masthead { 38 | padding-top: calc(1rem + 106px); 39 | padding-bottom: 6rem; 40 | } 41 | 42 | header .masthead h1 { 43 | font-size: 4.75em; 44 | line-height: 4rem; 45 | } 46 | 47 | header .masthead h2 { 48 | font-size: 1.75em; 49 | } 50 | } 51 | 52 | .img-profile { 53 | max-width: 100%; 54 | height: auto; 55 | margin-bottom: 3rem; 56 | display: block; 57 | margin-right: auto; 58 | margin-left: auto; 59 | } 60 | 61 | .title { 62 | font-size: small; 63 | } 64 | -------------------------------------------------------------------------------- /src/app/components/shell/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { MaterialModule } from '../../../material.module'; 6 | import { I18nService } from '../../../core'; 7 | import { HeaderComponent } from './header.component'; 8 | import { ConfigToken, DEFAULT_CONFIG } from '../../../../config'; 9 | 10 | describe('HeaderComponent', () => { 11 | let component: HeaderComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [RouterTestingModule, MaterialModule, TranslateModule.forRoot()], 17 | declarations: [HeaderComponent], 18 | providers: [ 19 | I18nService, 20 | { 21 | provide: ConfigToken, 22 | useValue: DEFAULT_CONFIG 23 | } 24 | ] 25 | }).compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(HeaderComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/components/shell/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, Input } from '@angular/core'; 2 | import { MatSidenav } from '@angular/material'; 3 | 4 | import { I18nService } from '../../../core'; 5 | import { Config, ConfigToken } from '../../../../config'; 6 | import { DOCUMENT } from '@angular/common'; 7 | 8 | @Component({ 9 | selector: 'app-header', 10 | templateUrl: './header.component.html', 11 | styleUrls: ['./header.component.scss'] 12 | }) 13 | export class HeaderComponent { 14 | @Input() sidenav: MatSidenav; 15 | 16 | constructor( 17 | @Inject(ConfigToken) public config: Config, 18 | @Inject(DOCUMENT) private document: any, 19 | private i18nService: I18nService 20 | ) {} 21 | 22 | setLanguage(language: string) { 23 | this.i18nService.language = language; 24 | } 25 | 26 | get currentLanguage(): string { 27 | return this.i18nService.language; 28 | } 29 | 30 | get languages(): string[] { 31 | return this.i18nService.supportedLanguages; 32 | } 33 | 34 | scrollToElementByID(id: string): void { 35 | document.getElementById(id).scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | 3 | :host { 4 | display: flex; 5 | flex: 1; 6 | } 7 | 8 | .mat-sidenav { 9 | min-width: 270px; 10 | max-width: 28%; 11 | } 12 | 13 | .has-border { 14 | border-right: 1px solid rgba(0, 0, 0, 0.12); 15 | } 16 | 17 | .mat-list { 18 | padding-top: 0; 19 | } 20 | 21 | .mat-list-item { 22 | text-decoration: none; 23 | 24 | &:hover { 25 | background: rgba(0, 0, 0, 0.05); 26 | } 27 | } 28 | 29 | .mat-sidenav, 30 | .mat-drawer-content, 31 | // Force style down to child components 32 | // See https://angular.io/guide/component-styles#deep 33 | :host ::ng-deep .mat-drawer-backdrop { 34 | top: 64px; 35 | bottom: 0; 36 | left: 0; 37 | right: 0; 38 | height: auto; 39 | position: absolute; 40 | } 41 | 42 | @media ($mat-xsmall) { 43 | .mat-sidenav, 44 | .mat-drawer-content, 45 | :host ::ng-deep .mat-drawer-backdrop { 46 | top: 56px; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { FlexLayoutModule } from '@angular/flex-layout'; 6 | import { MaterialModule } from '../../material.module'; 7 | 8 | import { CoreModule } from '../../core'; 9 | 10 | import { ShellComponent } from './shell.component'; 11 | import { HeaderComponent } from './header/header.component'; 12 | import { ConfigToken, DEFAULT_CONFIG } from '../../../config'; 13 | 14 | describe('ShellComponent', () => { 15 | let component: ShellComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | RouterTestingModule, 22 | TranslateModule.forRoot(), 23 | BrowserAnimationsModule, 24 | FlexLayoutModule, 25 | MaterialModule, 26 | CoreModule 27 | ], 28 | declarations: [HeaderComponent, ShellComponent], 29 | providers: [ 30 | { 31 | provide: ConfigToken, 32 | useValue: DEFAULT_CONFIG 33 | } 34 | ] 35 | }).compileComponents(); 36 | })); 37 | 38 | beforeEach(() => { 39 | fixture = TestBed.createComponent(ShellComponent); 40 | component = fixture.componentInstance; 41 | fixture.detectChanges(); 42 | }); 43 | 44 | it('should create', () => { 45 | expect(component).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { MediaChange, MediaObserver } from '@angular/flex-layout'; 3 | import { MatSidenav } from '@angular/material'; 4 | import { filter } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-shell', 8 | templateUrl: './shell.component.html', 9 | styleUrls: ['./shell.component.scss'] 10 | }) 11 | export class ShellComponent implements OnInit { 12 | @ViewChild('sidenav') sidenav: MatSidenav; 13 | 14 | constructor(private media: MediaObserver) {} 15 | 16 | ngOnInit() { 17 | // Automatically close side menu on screens > sm breakpoint 18 | this.media.media$ 19 | .pipe(filter((change: MediaChange) => change.mqAlias !== 'xs' && change.mqAlias !== 'sm')) 20 | .subscribe(() => this.sidenav.close()); 21 | } 22 | 23 | scrollToElementByID(id: string): void { 24 | document.getElementById(id).scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { ShellModule } from './shell.module'; 2 | 3 | describe('ShellModule', () => { 4 | let shellModule: ShellModule; 5 | 6 | beforeEach(() => { 7 | shellModule = new ShellModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(shellModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { ShellComponent } from './shell.component'; 7 | import { HeaderComponent } from './header/header.component'; 8 | import { SharedModule } from '../../shared'; 9 | 10 | @NgModule({ 11 | imports: [CommonModule, TranslateModule, SharedModule, RouterModule], 12 | declarations: [HeaderComponent, ShellComponent] 13 | }) 14 | export class ShellModule {} 15 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ShellComponent } from './shell.component'; 4 | import { Shell } from './shell.service'; 5 | 6 | describe('Shell', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [ShellComponent] 10 | }); 11 | }); 12 | 13 | describe('childRoutes', () => { 14 | it('should create routes as children of shell', () => { 15 | // Prepare 16 | const testRoutes = [{ path: 'test' }]; 17 | 18 | // Act 19 | const result = Shell.childRoutes(testRoutes); 20 | 21 | // Assert 22 | expect(result.path).toBe(''); 23 | expect(result.children).toBe(testRoutes); 24 | expect(result.component).toBe(ShellComponent); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/components/shell/shell.service.ts: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from '@angular/router'; 2 | 3 | import { ShellComponent } from './shell.component'; 4 | 5 | /** 6 | * Provides helper methods to create routes. 7 | */ 8 | export class Shell { 9 | /** 10 | * Creates routes using the shell component and authentication. 11 | * @param routes The routes to add. 12 | * @return The new route using shell as the base. 13 | */ 14 | static childRoutes(routes: Routes): Route { 15 | return { 16 | path: '', 17 | component: ShellComponent, 18 | children: routes, 19 | // Reuse ShellComponent instance when navigating between child views 20 | data: { reuse: true } 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 4 | import { RouteReuseStrategy, RouterModule } from '@angular/router'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | import { RouteReusableStrategy } from './route-reusable-strategy'; 7 | import { I18nService } from './i18n.service'; 8 | import { HttpService } from './http/http.service'; 9 | import { HttpCacheService } from './http/http-cache.service'; 10 | import { ErrorHandlerInterceptor } from './http/error-handler.interceptor'; 11 | import { CacheInterceptor } from './http/cache.interceptor'; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, HttpClientModule, TranslateModule, RouterModule], 15 | providers: [ 16 | I18nService, 17 | HttpCacheService, 18 | ErrorHandlerInterceptor, 19 | CacheInterceptor, 20 | { 21 | provide: HttpClient, 22 | useClass: HttpService 23 | }, 24 | { 25 | provide: RouteReuseStrategy, 26 | useClass: RouteReusableStrategy 27 | } 28 | ] 29 | }) 30 | export class CoreModule { 31 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) { 32 | // Import guard 33 | if (parentModule) { 34 | throw new Error(`${parentModule} has already been loaded. Import Core module in the AppModule only.`); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/core/http/api-prefix.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 4 | 5 | import { environment } from '@env/environment'; 6 | import { ApiPrefixInterceptor } from './api-prefix.interceptor'; 7 | 8 | describe('ApiPrefixInterceptor', () => { 9 | let http: HttpClient; 10 | let httpMock: HttpTestingController; 11 | 12 | beforeEach(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [HttpClientTestingModule], 15 | providers: [ 16 | { 17 | provide: HTTP_INTERCEPTORS, 18 | useClass: ApiPrefixInterceptor, 19 | multi: true 20 | } 21 | ] 22 | }); 23 | }); 24 | 25 | beforeEach(inject([HttpClient, HttpTestingController], (_http: HttpClient, _httpMock: HttpTestingController) => { 26 | http = _http; 27 | httpMock = _httpMock; 28 | })); 29 | 30 | afterEach(() => { 31 | httpMock.verify(); 32 | }); 33 | 34 | it('should prepend environment.serverUrl to the request url', () => { 35 | // Act 36 | http.get('/toto').subscribe(); 37 | 38 | // Assert 39 | httpMock.expectOne({ url: environment.serverUrl + '/toto' }); 40 | }); 41 | 42 | it('should not prepend environment.serverUrl to request url', () => { 43 | // Act 44 | http.get('hTtPs://domain.com/toto').subscribe(); 45 | 46 | // Assert 47 | httpMock.expectOne({ url: 'hTtPs://domain.com/toto' }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/core/http/api-prefix.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@env/environment'; 6 | 7 | /** 8 | * Prefixes all requests with `environment.serverUrl`. 9 | */ 10 | @Injectable() 11 | export class ApiPrefixInterceptor implements HttpInterceptor { 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | if (!/^(http|https):/i.test(request.url)) { 14 | request = request.clone({ url: environment.serverUrl + request.url }); 15 | } 16 | return next.handle(request); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/core/http/cache.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | import { HTTP_INTERCEPTORS, HttpClient, HttpResponse } from '@angular/common/http'; 4 | 5 | import { CacheInterceptor } from './cache.interceptor'; 6 | import { HttpCacheService } from './http-cache.service'; 7 | 8 | describe('CacheInterceptor', () => { 9 | let interceptorOptions: Object | null = {}; 10 | let httpCacheService: HttpCacheService; 11 | let cacheInterceptor: CacheInterceptor; 12 | let http: HttpClient; 13 | let httpMock: HttpTestingController; 14 | 15 | function createInterceptor(_httpCacheService: HttpCacheService) { 16 | cacheInterceptor = new CacheInterceptor(_httpCacheService).configure(interceptorOptions); 17 | return cacheInterceptor; 18 | } 19 | 20 | beforeEach(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [HttpClientTestingModule], 23 | providers: [ 24 | HttpCacheService, 25 | { 26 | provide: HTTP_INTERCEPTORS, 27 | useFactory: createInterceptor, 28 | deps: [HttpCacheService], 29 | multi: true 30 | } 31 | ] 32 | }); 33 | }); 34 | 35 | afterEach(() => { 36 | httpCacheService.cleanCache(); 37 | httpMock.verify(); 38 | }); 39 | 40 | describe('with default configuration', () => { 41 | beforeEach(() => { 42 | interceptorOptions = null; 43 | }); 44 | 45 | beforeEach(inject( 46 | [HttpClient, HttpTestingController, HttpCacheService], 47 | (_http: HttpClient, _httpMock: HttpTestingController, _httpCacheService: HttpCacheService) => { 48 | http = _http; 49 | httpMock = _httpMock; 50 | httpCacheService = _httpCacheService; 51 | } 52 | )); 53 | 54 | it('should cache the request', () => { 55 | // Act 56 | http.get('/toto').subscribe(() => { 57 | // Assert 58 | const cachedData = httpCacheService.getCacheData('/toto'); 59 | expect(cachedData).toBeDefined(); 60 | expect(cachedData ? cachedData.body : null).toEqual('someData'); 61 | }); 62 | 63 | httpMock.expectOne({ url: '/toto' }).flush('someData'); 64 | }); 65 | 66 | it('should respond from the cache', () => { 67 | // Arrange 68 | httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'cachedData' })); 69 | 70 | // Act 71 | http.get('/toto').subscribe(response => { 72 | // Assert 73 | expect(response).toEqual('cachedData'); 74 | }); 75 | 76 | httpMock.expectNone({ url: '/toto' }); 77 | }); 78 | 79 | it('should not cache the request in case of error', () => { 80 | // Act 81 | http.get('/toto').subscribe( 82 | () => {}, 83 | () => { 84 | // Assert 85 | expect(httpCacheService.getCacheData('/toto')).toBeNull(); 86 | } 87 | ); 88 | 89 | httpMock.expectOne({}).flush(null, { 90 | status: 404, 91 | statusText: 'error' 92 | }); 93 | }); 94 | }); 95 | 96 | describe('with update forced configuration', () => { 97 | beforeEach(() => { 98 | interceptorOptions = { update: true }; 99 | }); 100 | 101 | beforeEach(inject( 102 | [HttpClient, HttpTestingController, HttpCacheService], 103 | (_http: HttpClient, _httpMock: HttpTestingController, _httpCacheService: HttpCacheService) => { 104 | http = _http; 105 | httpMock = _httpMock; 106 | httpCacheService = _httpCacheService; 107 | } 108 | )); 109 | 110 | afterEach(() => { 111 | httpCacheService.cleanCache(); 112 | httpMock.verify(); 113 | }); 114 | 115 | it('should force cache update', () => { 116 | // Arrange 117 | httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'oldCachedData' })); 118 | // cacheInterceptor.configure({ update: true }); 119 | 120 | // Act 121 | http.get('/toto').subscribe(response => { 122 | // Assert 123 | expect(response).toEqual('newData'); 124 | }); 125 | 126 | httpMock.expectOne({ url: '/toto' }).flush('newData'); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/app/core/http/cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; 3 | import { Observable, Subscriber } from 'rxjs'; 4 | 5 | import { HttpCacheService } from './http-cache.service'; 6 | 7 | /** 8 | * Caches HTTP requests. 9 | * Use ExtendedHttpClient fluent API to configure caching for each request. 10 | */ 11 | @Injectable() 12 | export class CacheInterceptor implements HttpInterceptor { 13 | private forceUpdate = false; 14 | 15 | constructor(private httpCacheService: HttpCacheService) {} 16 | 17 | /** 18 | * Configures interceptor options 19 | * @param options If update option is enabled, forces request to be made and updates cache entry. 20 | * @return The configured instance. 21 | */ 22 | configure(options?: { update?: boolean } | null): CacheInterceptor { 23 | const instance = new CacheInterceptor(this.httpCacheService); 24 | if (options && options.update) { 25 | instance.forceUpdate = true; 26 | } 27 | return instance; 28 | } 29 | 30 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 31 | if (request.method !== 'GET') { 32 | return next.handle(request); 33 | } 34 | 35 | return new Observable((subscriber: Subscriber>) => { 36 | const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams); 37 | if (cachedData !== null) { 38 | // Create new response to avoid side-effects 39 | subscriber.next(new HttpResponse(cachedData as Object)); 40 | subscriber.complete(); 41 | } else { 42 | next.handle(request).subscribe( 43 | event => { 44 | if (event instanceof HttpResponse) { 45 | this.httpCacheService.setCacheData(request.urlWithParams, event); 46 | } 47 | subscriber.next(event); 48 | }, 49 | error => subscriber.error(error), 50 | () => subscriber.complete() 51 | ); 52 | } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/core/http/error-handler.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; 4 | 5 | import { ErrorHandlerInterceptor } from './error-handler.interceptor'; 6 | 7 | describe('ErrorHandlerInterceptor', () => { 8 | let errorHandlerInterceptor: ErrorHandlerInterceptor; 9 | let http: HttpClient; 10 | let httpMock: HttpTestingController; 11 | 12 | function createInterceptor() { 13 | errorHandlerInterceptor = new ErrorHandlerInterceptor(); 14 | return errorHandlerInterceptor; 15 | } 16 | 17 | beforeEach(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [HttpClientTestingModule], 20 | providers: [ 21 | { 22 | provide: HTTP_INTERCEPTORS, 23 | useFactory: createInterceptor, 24 | multi: true 25 | } 26 | ] 27 | }); 28 | }); 29 | 30 | beforeEach(inject([HttpClient, HttpTestingController], (_http: HttpClient, _httpMock: HttpTestingController) => { 31 | http = _http; 32 | httpMock = _httpMock; 33 | })); 34 | 35 | afterEach(() => { 36 | httpMock.verify(); 37 | }); 38 | 39 | it('should catch error and call error handler', () => { 40 | // Arrange 41 | // Note: here we spy on private method since target is customization here, 42 | // but you should replace it by actual behavior in your app 43 | spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler').and.callThrough(); 44 | 45 | // Act 46 | http.get('/toto').subscribe( 47 | () => fail('should error'), 48 | () => { 49 | // Assert 50 | expect(ErrorHandlerInterceptor.prototype['errorHandler']).toHaveBeenCalled(); 51 | } 52 | ); 53 | 54 | httpMock.expectOne({}).flush(null, { 55 | status: 404, 56 | statusText: 'error' 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/core/http/error-handler.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { environment } from '@env/environment'; 7 | import { Logger } from '../logger.service'; 8 | 9 | const log = new Logger('ErrorHandlerInterceptor'); 10 | 11 | /** 12 | * Adds a default error handler to all requests. 13 | */ 14 | @Injectable() 15 | export class ErrorHandlerInterceptor implements HttpInterceptor { 16 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 17 | return next.handle(request).pipe(catchError(error => this.errorHandler(error))); 18 | } 19 | 20 | // Customize the default error handler here if needed 21 | private errorHandler(response: HttpEvent): Observable> { 22 | if (!environment.production) { 23 | // Do something with the error 24 | log.error('Request error', response); 25 | } 26 | throw response; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/core/http/http-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpResponse } from '@angular/common/http'; 3 | import { each } from 'lodash'; 4 | 5 | import { Logger } from '../logger.service'; 6 | 7 | const log = new Logger('HttpCacheService'); 8 | const cachePersistenceKey = 'httpCache'; 9 | 10 | export interface HttpCacheEntry { 11 | lastUpdated: Date; 12 | data: HttpResponse; 13 | } 14 | 15 | /** 16 | * Provides a cache facility for HTTP requests with configurable persistence policy. 17 | */ 18 | @Injectable() 19 | export class HttpCacheService { 20 | private cachedData: { [key: string]: HttpCacheEntry } = {}; 21 | private storage: Storage | null = null; 22 | 23 | constructor() { 24 | this.loadCacheData(); 25 | } 26 | 27 | /** 28 | * Sets the cache data for the specified request. 29 | * @param url The request URL. 30 | * @param data The received data. 31 | * @param lastUpdated The cache last update, current date is used if not specified. 32 | */ 33 | setCacheData(url: string, data: HttpResponse, lastUpdated?: Date) { 34 | this.cachedData[url] = { 35 | lastUpdated: lastUpdated || new Date(), 36 | data: data 37 | }; 38 | log.debug(`Cache set for key: "${url}"`); 39 | this.saveCacheData(); 40 | } 41 | 42 | /** 43 | * Gets the cached data for the specified request. 44 | * @param url The request URL. 45 | * @return The cached data or null if no cached data exists for this request. 46 | */ 47 | getCacheData(url: string): HttpResponse | null { 48 | const cacheEntry = this.cachedData[url]; 49 | 50 | if (cacheEntry) { 51 | log.debug(`Cache hit for key: "${url}"`); 52 | return cacheEntry.data; 53 | } 54 | 55 | return null; 56 | } 57 | 58 | /** 59 | * Gets the cached entry for the specified request. 60 | * @param url The request URL. 61 | * @return The cache entry or null if no cache entry exists for this request. 62 | */ 63 | getHttpCacheEntry(url: string): HttpCacheEntry | null { 64 | return this.cachedData[url] || null; 65 | } 66 | 67 | /** 68 | * Clears the cached entry (if exists) for the specified request. 69 | * @param url The request URL. 70 | */ 71 | clearCache(url: string): void { 72 | delete this.cachedData[url]; 73 | log.debug(`Cache cleared for key: "${url}"`); 74 | this.saveCacheData(); 75 | } 76 | 77 | /** 78 | * Cleans cache entries older than the specified date. 79 | * @param expirationDate The cache expiration date. If no date is specified, all cache is cleared. 80 | */ 81 | cleanCache(expirationDate?: Date) { 82 | if (expirationDate) { 83 | each(this.cachedData, (value: HttpCacheEntry, key: string) => { 84 | if (expirationDate >= value.lastUpdated) { 85 | delete this.cachedData[key]; 86 | } 87 | }); 88 | } else { 89 | this.cachedData = {}; 90 | } 91 | this.saveCacheData(); 92 | } 93 | 94 | /** 95 | * Sets the cache persistence policy. 96 | * Note that changing the cache persistence will also clear the cache from its previous storage. 97 | * @param persistence How the cache should be persisted, it can be either local or session storage, or if no value is 98 | * provided it will be only in-memory (default). 99 | */ 100 | setPersistence(persistence?: 'local' | 'session') { 101 | this.cleanCache(); 102 | this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null; 103 | this.loadCacheData(); 104 | } 105 | 106 | private saveCacheData() { 107 | if (this.storage) { 108 | this.storage[cachePersistenceKey] = JSON.stringify(this.cachedData); 109 | } 110 | } 111 | 112 | private loadCacheData() { 113 | const data = this.storage ? this.storage[cachePersistenceKey] : null; 114 | this.cachedData = data ? JSON.parse(data) : {}; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/app/core/http/http.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | import { HttpClient, HttpInterceptor } from '@angular/common/http'; 4 | 5 | import { HttpService } from './http.service'; 6 | import { HttpCacheService } from './http-cache.service'; 7 | import { ErrorHandlerInterceptor } from './error-handler.interceptor'; 8 | import { CacheInterceptor } from './cache.interceptor'; 9 | 10 | describe('HttpService', () => { 11 | let httpCacheService: HttpCacheService; 12 | let http: HttpClient; 13 | let httpMock: HttpTestingController; 14 | 15 | beforeEach(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [HttpClientTestingModule], 18 | providers: [ 19 | ErrorHandlerInterceptor, 20 | CacheInterceptor, 21 | HttpCacheService, 22 | { 23 | provide: HttpClient, 24 | useClass: HttpService 25 | } 26 | ] 27 | }); 28 | }); 29 | 30 | beforeEach(inject( 31 | [HttpClient, HttpTestingController, HttpCacheService], 32 | (_http: HttpClient, _httpMock: HttpTestingController, _httpCacheService: HttpCacheService) => { 33 | http = _http; 34 | httpMock = _httpMock; 35 | httpCacheService = _httpCacheService; 36 | } 37 | )); 38 | 39 | afterEach(() => { 40 | httpCacheService.cleanCache(); 41 | httpMock.verify(); 42 | }); 43 | 44 | it('should use error handler, API prefix and no cache by default', () => { 45 | // Arrange 46 | let interceptors: HttpInterceptor[]; 47 | const realRequest = http.request; 48 | spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) { 49 | interceptors = this.interceptors; 50 | return realRequest.apply(this, arguments); 51 | }); 52 | 53 | // Act 54 | const request = http.get('/toto'); 55 | 56 | // Assert 57 | request.subscribe(() => { 58 | expect(http.request).toHaveBeenCalled(); 59 | expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); 60 | expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); 61 | }); 62 | httpMock.expectOne({}).flush({}); 63 | }); 64 | 65 | it('should use cache', () => { 66 | // Arrange 67 | let interceptors: HttpInterceptor[]; 68 | const realRequest = http.request; 69 | spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) { 70 | interceptors = this.interceptors; 71 | return realRequest.apply(this, arguments); 72 | }); 73 | 74 | // Act 75 | const request = http.cache().get('/toto'); 76 | 77 | // Assert 78 | request.subscribe(() => { 79 | expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy(); 80 | expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeTruthy(); 81 | }); 82 | httpMock.expectOne({}).flush({}); 83 | }); 84 | 85 | it('should skip error handler', () => { 86 | // Arrange 87 | let interceptors: HttpInterceptor[]; 88 | const realRequest = http.request; 89 | spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) { 90 | interceptors = this.interceptors; 91 | return realRequest.apply(this, arguments); 92 | }); 93 | 94 | // Act 95 | const request = http.skipErrorHandler().get('/toto'); 96 | 97 | // Assert 98 | request.subscribe(() => { 99 | expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeFalsy(); 100 | expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy(); 101 | }); 102 | httpMock.expectOne({}).flush({}); 103 | }); 104 | 105 | it('should not use API prefix', () => { 106 | // Arrange 107 | let interceptors: HttpInterceptor[]; 108 | const realRequest = http.request; 109 | spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) { 110 | interceptors = this.interceptors; 111 | return realRequest.apply(this, arguments); 112 | }); 113 | 114 | httpMock.expectOne({}).flush({}); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/app/core/http/http.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core'; 2 | import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { ErrorHandlerInterceptor } from './error-handler.interceptor'; 6 | import { CacheInterceptor } from './cache.interceptor'; 7 | 8 | // HttpClient is declared in a re-exported module, so we have to extend the original module to make it work properly 9 | // (see https://github.com/Microsoft/TypeScript/issues/13897) 10 | declare module '@angular/common/http/src/client' { 11 | // Augment HttpClient with the added configuration methods from HttpService, to allow in-place replacement of 12 | // HttpClient with HttpService using dependency injection 13 | export interface HttpClient { 14 | /** 15 | * Enables caching for this request. 16 | * @param forceUpdate Forces request to be made and updates cache entry. 17 | * @return The new instance. 18 | */ 19 | cache(forceUpdate?: boolean): HttpClient; 20 | 21 | /** 22 | * Skips default error handler for this request. 23 | * @return The new instance. 24 | */ 25 | skipErrorHandler(): HttpClient; 26 | } 27 | } 28 | 29 | // From @angular/common/http/src/interceptor: allows to chain interceptors 30 | class HttpInterceptorHandler implements HttpHandler { 31 | constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {} 32 | 33 | handle(request: HttpRequest): Observable> { 34 | return this.interceptor.intercept(request, this.next); 35 | } 36 | } 37 | 38 | /** 39 | * Allows to override default dynamic interceptors that can be disabled with the HttpService extension. 40 | * Except for very specific needs, you should better configure these interceptors directly in the constructor below 41 | * for better readability. 42 | * 43 | * For static interceptors that should always be enabled (like ApiPrefixInterceptor), use the standard 44 | * HTTP_INTERCEPTORS token. 45 | */ 46 | export const HTTP_DYNAMIC_INTERCEPTORS = new InjectionToken('HTTP_DYNAMIC_INTERCEPTORS'); 47 | 48 | /** 49 | * Extends HttpClient with per request configuration using dynamic interceptors. 50 | */ 51 | @Injectable() 52 | export class HttpService extends HttpClient { 53 | constructor( 54 | private httpHandler: HttpHandler, 55 | private injector: Injector, 56 | @Optional() @Inject(HTTP_DYNAMIC_INTERCEPTORS) private interceptors: HttpInterceptor[] = [] 57 | ) { 58 | super(httpHandler); 59 | 60 | if (!this.interceptors) { 61 | // Configure default interceptors that can be disabled here 62 | this.interceptors = [this.injector.get(ErrorHandlerInterceptor)]; 63 | } 64 | } 65 | 66 | cache(forceUpdate?: boolean): HttpClient { 67 | const cacheInterceptor = this.injector.get(CacheInterceptor).configure({ update: forceUpdate }); 68 | return this.addInterceptor(cacheInterceptor); 69 | } 70 | 71 | skipErrorHandler(): HttpClient { 72 | return this.removeInterceptor(ErrorHandlerInterceptor); 73 | } 74 | 75 | // Override the original method to wire interceptors when triggering the request. 76 | request(method?: any, url?: any, options?: any): any { 77 | const handler = this.interceptors.reduceRight( 78 | (next, interceptor) => new HttpInterceptorHandler(next, interceptor), 79 | this.httpHandler 80 | ); 81 | return new HttpClient(handler).request(method, url, options); 82 | } 83 | 84 | private removeInterceptor(interceptorType: Function): HttpService { 85 | return new HttpService( 86 | this.httpHandler, 87 | this.injector, 88 | this.interceptors.filter(i => !(i instanceof interceptorType)) 89 | ); 90 | } 91 | 92 | private addInterceptor(interceptor: HttpInterceptor): HttpService { 93 | return new HttpService(this.httpHandler, this.injector, this.interceptors.concat([interceptor])); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/core/i18n.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; 3 | import { Subject } from 'rxjs'; 4 | 5 | import { extract, I18nService } from './i18n.service'; 6 | 7 | const defaultLanguage = 'en-US'; 8 | const supportedLanguages = ['eo', 'en-US', 'fr-FR']; 9 | 10 | class MockTranslateService { 11 | currentLang: string; 12 | onLangChange = new Subject(); 13 | 14 | use(language: string) { 15 | this.currentLang = language; 16 | this.onLangChange.next({ 17 | lang: this.currentLang, 18 | translations: {} 19 | }); 20 | } 21 | 22 | getBrowserCultureLang() { 23 | return 'en-US'; 24 | } 25 | 26 | setTranslation(lang: string, translations: Object, shouldMerge?: boolean) {} 27 | } 28 | 29 | describe('I18nService', () => { 30 | let i18nService: I18nService; 31 | let translateService: TranslateService; 32 | let onLangChangeSpy: jasmine.Spy; 33 | 34 | beforeEach(() => { 35 | TestBed.configureTestingModule({ 36 | providers: [I18nService, { provide: TranslateService, useClass: MockTranslateService }] 37 | }); 38 | }); 39 | 40 | beforeEach(inject( 41 | [I18nService, TranslateService], 42 | (_i18nService: I18nService, _translateService: TranslateService) => { 43 | i18nService = _i18nService; 44 | translateService = _translateService; 45 | 46 | // Create spies 47 | onLangChangeSpy = jasmine.createSpy('onLangChangeSpy'); 48 | translateService.onLangChange.subscribe((event: LangChangeEvent) => { 49 | onLangChangeSpy(event.lang); 50 | }); 51 | spyOn(translateService, 'use').and.callThrough(); 52 | } 53 | )); 54 | 55 | afterEach(() => { 56 | // Cleanup 57 | localStorage.removeItem('language'); 58 | }); 59 | 60 | describe('extract', () => { 61 | it('should not modify string', () => { 62 | expect(extract('Hello world !')).toEqual('Hello world !'); 63 | }); 64 | }); 65 | 66 | describe('init', () => { 67 | it('should init with default language', () => { 68 | // Act 69 | i18nService.init(defaultLanguage, supportedLanguages); 70 | 71 | // Assert 72 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); 73 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); 74 | }); 75 | 76 | it('should init with save language', () => { 77 | // Arrange 78 | const savedLanguage = 'eo'; 79 | localStorage.setItem('language', savedLanguage); 80 | 81 | // Act 82 | i18nService.init(defaultLanguage, supportedLanguages); 83 | 84 | // Assert 85 | expect(translateService.use).toHaveBeenCalledWith(savedLanguage); 86 | expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage); 87 | }); 88 | }); 89 | 90 | describe('set language', () => { 91 | it('should change current language', () => { 92 | // Arrange 93 | const newLanguage = 'eo'; 94 | i18nService.init(defaultLanguage, supportedLanguages); 95 | 96 | // Act 97 | i18nService.language = newLanguage; 98 | 99 | // Assert 100 | expect(translateService.use).toHaveBeenCalledWith(newLanguage); 101 | expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage); 102 | }); 103 | 104 | it('should change current language without a region match', () => { 105 | // Arrange 106 | const newLanguage = 'fr-CA'; 107 | i18nService.init(defaultLanguage, supportedLanguages); 108 | 109 | // Act 110 | i18nService.language = newLanguage; 111 | 112 | // Assert 113 | expect(translateService.use).toHaveBeenCalledWith('fr-FR'); 114 | expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR'); 115 | }); 116 | 117 | it('should change current language to default if unsupported', () => { 118 | // Arrange 119 | const newLanguage = 'es'; 120 | i18nService.init(defaultLanguage, supportedLanguages); 121 | 122 | // Act 123 | i18nService.language = newLanguage; 124 | 125 | // Assert 126 | expect(translateService.use).toHaveBeenCalledWith(defaultLanguage); 127 | expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage); 128 | }); 129 | }); 130 | 131 | describe('get language', () => { 132 | it('should return current language', () => { 133 | // Arrange 134 | i18nService.init(defaultLanguage, supportedLanguages); 135 | 136 | // Act 137 | const currentLanguage = i18nService.language; 138 | 139 | // Assert 140 | expect(currentLanguage).toEqual(defaultLanguage); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/app/core/i18n.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { LangChangeEvent, TranslateService } from '@ngx-translate/core'; 3 | import { includes } from 'lodash'; 4 | 5 | import { Logger } from './logger.service'; 6 | import { LocalStorage } from 'ngx-webstorage'; 7 | import enUS from '../../translations/en-US.json'; 8 | import frFR from '../../translations/fr-FR.json'; 9 | import deDE from '../../translations/de-DE.json'; 10 | 11 | const log = new Logger('I18nService'); 12 | const languageKey = 'language'; 13 | 14 | /** 15 | * Pass-through function to mark a string for translation extraction. 16 | * Running `npm translations:extract` will include the given string by using this. 17 | * @param s The string to extract for translation. 18 | * @return The same string. 19 | */ 20 | export function extract(s: string) { 21 | return s; 22 | } 23 | 24 | @Injectable() 25 | export class I18nService { 26 | @LocalStorage() 27 | _language: string; 28 | 29 | defaultLanguage: string; 30 | supportedLanguages: string[]; 31 | 32 | constructor(private translateService: TranslateService) { 33 | // Embed languages to avoid extra HTTP requests 34 | translateService.setTranslation('en-US', enUS); 35 | translateService.setTranslation('fr-FR', frFR); 36 | translateService.setTranslation('de-DE', deDE); 37 | } 38 | 39 | /** 40 | * Sets the current language. 41 | * Note: The current language is saved to the local storage. 42 | * If no parameter is specified, the language is loaded from local storage (if present). 43 | * @param language The IETF language code to set. 44 | */ 45 | set language(language: string) { 46 | language = language || this._language || this.translateService.getBrowserCultureLang(); 47 | let isSupportedLanguage = includes(this.supportedLanguages, language); 48 | 49 | // If no exact match is found, search without the region 50 | if (language && !isSupportedLanguage) { 51 | language = language.split('-')[0]; 52 | language = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(language)) || ''; 53 | isSupportedLanguage = Boolean(language); 54 | } 55 | 56 | // Fallback if language is not supported 57 | if (!isSupportedLanguage) { 58 | language = this.defaultLanguage; 59 | } 60 | 61 | log.debug(`Language set to ${language}`); 62 | this.translateService.use(language); 63 | } 64 | 65 | /** 66 | * Initializes i18n for the application. 67 | * Loads language from local storage if present, or sets default language. 68 | * @param defaultLanguage The default language to use. 69 | * @param supportedLanguages The list of supported languages. 70 | */ 71 | init(defaultLanguage: string, supportedLanguages: string[]) { 72 | this.defaultLanguage = defaultLanguage; 73 | this.supportedLanguages = supportedLanguages; 74 | this.language = ''; 75 | 76 | this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { 77 | this._language = event.lang; 78 | }); 79 | } 80 | 81 | /** 82 | * Gets the current language. 83 | * @return The current language code. 84 | */ 85 | get language(): string { 86 | return this.translateService.currentLang; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core.module'; 2 | export * from './i18n.service'; 3 | export * from './http/http.service'; 4 | export * from './http/http-cache.service'; 5 | export * from './http/cache.interceptor'; 6 | export * from './http/error-handler.interceptor'; 7 | export * from './route-reusable-strategy'; 8 | export * from './logger.service'; 9 | -------------------------------------------------------------------------------- /src/app/core/logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel, LogOutput } from './logger.service'; 2 | 3 | const logMethods = ['log', 'info', 'warn', 'error']; 4 | 5 | describe('Logger', () => { 6 | let savedConsole: Function[]; 7 | let savedLevel: LogLevel; 8 | let savedOutputs: LogOutput[]; 9 | 10 | beforeAll(() => { 11 | savedConsole = []; 12 | logMethods.forEach(m => { 13 | savedConsole[m] = console[m]; 14 | console[m] = () => {}; 15 | }); 16 | savedLevel = Logger.level; 17 | savedOutputs = Logger.outputs; 18 | }); 19 | 20 | afterAll(() => { 21 | logMethods.forEach(m => { 22 | console[m] = savedConsole[m]; 23 | }); 24 | Logger.level = savedLevel; 25 | Logger.outputs = savedOutputs; 26 | }); 27 | 28 | it('should create an instance', () => { 29 | expect(new Logger()).toBeTruthy(); 30 | }); 31 | 32 | it('should add a new LogOutput and receives log entries', () => { 33 | // Arrange 34 | const outputSpy = jasmine.createSpy('outputSpy'); 35 | const log = new Logger('test'); 36 | 37 | // Act 38 | Logger.outputs.push(outputSpy); 39 | 40 | log.debug('d'); 41 | log.info('i'); 42 | log.warn('w'); 43 | log.error('e', { error: true }); 44 | 45 | // Assert 46 | expect(outputSpy).toHaveBeenCalled(); 47 | expect(outputSpy.calls.count()).toBe(4); 48 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd'); 49 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i'); 50 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); 51 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); 52 | }); 53 | 54 | it('should add a new LogOutput and receives only production log entries', () => { 55 | // Arrange 56 | const outputSpy = jasmine.createSpy('outputSpy'); 57 | const log = new Logger('test'); 58 | 59 | // Act 60 | Logger.outputs.push(outputSpy); 61 | Logger.enableProductionMode(); 62 | 63 | log.debug('d'); 64 | log.info('i'); 65 | log.warn('w'); 66 | log.error('e', { error: true }); 67 | 68 | // Assert 69 | expect(outputSpy).toHaveBeenCalled(); 70 | expect(outputSpy.calls.count()).toBe(2); 71 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w'); 72 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/app/core/logger.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple logger system with the possibility of registering custom outputs. 3 | * 4 | * 4 different log levels are provided, with corresponding methods: 5 | * - debug : for debug information 6 | * - info : for informative status of the application (success, ...) 7 | * - warning : for non-critical errors that do not prevent normal application behavior 8 | * - error : for critical errors that prevent normal application behavior 9 | * 10 | * Example usage: 11 | * ``` 12 | * import { Logger } from 'app/core/logger.service'; 13 | * 14 | * const log = new Logger('myFile'); 15 | * ... 16 | * log.debug('something happened'); 17 | * ``` 18 | * 19 | * To disable debug and info logs in production, add this snippet to your root component: 20 | * ``` 21 | * export class AppComponent implements OnInit { 22 | * ngOnInit() { 23 | * if (environment.production) { 24 | * Logger.enableProductionMode(); 25 | * } 26 | * ... 27 | * } 28 | * } 29 | * 30 | * If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs. 31 | */ 32 | 33 | /** 34 | * The possible log levels. 35 | * LogLevel.Off is never emitted and only used with Logger.level property to disable logs. 36 | */ 37 | export enum LogLevel { 38 | Off = 0, 39 | Error, 40 | Warning, 41 | Info, 42 | Debug 43 | } 44 | 45 | /** 46 | * Log output handler function. 47 | */ 48 | export type LogOutput = (source: string, level: LogLevel, ...objects: any[]) => void; 49 | 50 | export class Logger { 51 | /** 52 | * Current logging level. 53 | * Set it to LogLevel.Off to disable logs completely. 54 | */ 55 | static level = LogLevel.Debug; 56 | 57 | /** 58 | * Additional log outputs. 59 | */ 60 | static outputs: LogOutput[] = []; 61 | 62 | /** 63 | * Enables production mode. 64 | * Sets logging level to LogLevel.Warning. 65 | */ 66 | static enableProductionMode() { 67 | Logger.level = LogLevel.Warning; 68 | } 69 | 70 | constructor(private source?: string) {} 71 | 72 | /** 73 | * Logs messages or objects with the debug level. 74 | * Works the same as console.log(). 75 | */ 76 | debug(...objects: any[]) { 77 | this.log(console.log, LogLevel.Debug, objects); 78 | } 79 | 80 | /** 81 | * Logs messages or objects with the info level. 82 | * Works the same as console.log(). 83 | */ 84 | info(...objects: any[]) { 85 | this.log(console.info, LogLevel.Info, objects); 86 | } 87 | 88 | /** 89 | * Logs messages or objects with the warning level. 90 | * Works the same as console.log(). 91 | */ 92 | warn(...objects: any[]) { 93 | this.log(console.warn, LogLevel.Warning, objects); 94 | } 95 | 96 | /** 97 | * Logs messages or objects with the error level. 98 | * Works the same as console.log(). 99 | */ 100 | error(...objects: any[]) { 101 | this.log(console.error, LogLevel.Error, objects); 102 | } 103 | 104 | private log(func: Function, level: LogLevel, objects: any[]) { 105 | if (level <= Logger.level) { 106 | const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects; 107 | func.apply(console, log); 108 | Logger.outputs.forEach(output => output.apply(output, [this.source, level].concat(objects))); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/app/core/route-reusable-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'; 2 | 3 | /** 4 | * A route strategy allowing for explicit route reuse. 5 | * Used as a workaround for https://github.com/angular/angular/issues/18374 6 | * To reuse a given route, add `data: { reuse: true }` to the route definition. 7 | */ 8 | export class RouteReusableStrategy extends RouteReuseStrategy { 9 | public shouldDetach(route: ActivatedRouteSnapshot): boolean { 10 | return false; 11 | } 12 | 13 | public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void {} 14 | 15 | public shouldAttach(route: ActivatedRouteSnapshot): boolean { 16 | return false; 17 | } 18 | 19 | public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { 20 | return null; 21 | } 22 | 23 | public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 24 | return future.routeConfig === curr.routeConfig || future.data.reuse; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { extract } from '@app/core'; 5 | import { HomeComponent } from './home.component'; 6 | import { Shell } from '@app/shell/shell.service'; 7 | 8 | const routes: Routes = [ 9 | Shell.childRoutes([ 10 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 11 | { path: 'home', component: HomeComponent, data: { title: extract('Home') } } 12 | ]) 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forChild(routes)], 17 | exports: [RouterModule], 18 | providers: [] 19 | }) 20 | export class HomeRoutingModule {} 21 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | Hello world ! 6 | 7 | 8 | {{ quote }} 9 | 10 | 11 | 19 | 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | padding: 1em; 4 | } 5 | 6 | .logo { 7 | width: 150px; 8 | margin: 0 auto; 9 | } 10 | 11 | q { 12 | font-style: italic; 13 | quotes: "« " " »"; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { Angulartics2Module } from 'angulartics2'; 7 | 8 | import { CoreModule } from '@app/core'; 9 | import { SharedModule } from '@app/shared'; 10 | import { MaterialModule } from '@app/material.module'; 11 | import { HomeComponent } from './home.component'; 12 | import { QuoteService } from './quote.service'; 13 | 14 | describe('HomeComponent', () => { 15 | let component: HomeComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [ 21 | BrowserAnimationsModule, 22 | FlexLayoutModule, 23 | MaterialModule, 24 | RouterTestingModule, 25 | Angulartics2Module.forRoot(), 26 | CoreModule, 27 | SharedModule, 28 | HttpClientTestingModule 29 | ], 30 | declarations: [HomeComponent], 31 | providers: [QuoteService] 32 | }).compileComponents(); 33 | })); 34 | 35 | beforeEach(() => { 36 | fixture = TestBed.createComponent(HomeComponent); 37 | component = fixture.componentInstance; 38 | fixture.detectChanges(); 39 | }); 40 | 41 | it('should create', () => { 42 | expect(component).toBeTruthy(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { finalize } from 'rxjs/operators'; 3 | 4 | import { QuoteService } from './quote.service'; 5 | 6 | @Component({ 7 | selector: 'app-home', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.scss'] 10 | }) 11 | export class HomeComponent implements OnInit { 12 | quote: string; 13 | isLoading: boolean; 14 | 15 | constructor(private quoteService: QuoteService) {} 16 | 17 | ngOnInit() { 18 | this.isLoading = true; 19 | this.quoteService 20 | .getRandomQuote({ category: 'dev' }) 21 | .pipe( 22 | finalize(() => { 23 | this.isLoading = false; 24 | }) 25 | ) 26 | .subscribe((quote: string) => { 27 | this.quote = quote; 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { Angulartics2Module } from 'angulartics2'; 6 | 7 | import { CoreModule } from '@app/core'; 8 | import { SharedModule } from '@app/shared'; 9 | import { MaterialModule } from '@app/material.module'; 10 | import { HomeRoutingModule } from './home-routing.module'; 11 | import { HomeComponent } from './home.component'; 12 | import { QuoteService } from './quote.service'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | TranslateModule, 18 | CoreModule, 19 | SharedModule, 20 | FlexLayoutModule, 21 | MaterialModule, 22 | Angulartics2Module, 23 | HomeRoutingModule 24 | ], 25 | declarations: [HomeComponent], 26 | providers: [QuoteService] 27 | }) 28 | export class HomeModule {} 29 | -------------------------------------------------------------------------------- /src/app/home/quote.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, inject, TestBed } from '@angular/core/testing'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | 4 | import { CoreModule, HttpCacheService } from '@app/core'; 5 | import { QuoteService } from './quote.service'; 6 | 7 | describe('QuoteService', () => { 8 | let quoteService: QuoteService; 9 | let httpMock: HttpTestingController; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [CoreModule, HttpClientTestingModule], 14 | providers: [HttpCacheService, QuoteService] 15 | }); 16 | })); 17 | 18 | beforeEach(inject( 19 | [HttpCacheService, QuoteService, HttpTestingController], 20 | (htttpCacheService: HttpCacheService, _quoteService: QuoteService, _httpMock: HttpTestingController) => { 21 | quoteService = _quoteService; 22 | httpMock = _httpMock; 23 | 24 | htttpCacheService.cleanCache(); 25 | } 26 | )); 27 | 28 | afterEach(() => { 29 | httpMock.verify(); 30 | }); 31 | 32 | describe('getRandomQuote', () => { 33 | it('should return a random Chuck Norris quote', () => { 34 | // Arrange 35 | const mockQuote = { value: 'a random quote' }; 36 | 37 | // Act 38 | const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); 39 | 40 | // Assert 41 | randomQuoteSubscription.subscribe((quote: string) => { 42 | expect(quote).toEqual(mockQuote.value); 43 | }); 44 | httpMock.expectOne({}).flush(mockQuote); 45 | }); 46 | 47 | it('should return a string in case of error', () => { 48 | // Act 49 | const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' }); 50 | 51 | // Assert 52 | randomQuoteSubscription.subscribe((quote: string) => { 53 | expect(typeof quote).toEqual('string'); 54 | expect(quote).toContain('Error'); 55 | }); 56 | httpMock.expectOne({}).flush(null, { 57 | status: 500, 58 | statusText: 'error' 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/home/quote.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | import { catchError, map } from 'rxjs/operators'; 5 | 6 | const routes = { 7 | quote: (c: RandomQuoteContext) => `/jokes/random?category=${c.category}` 8 | }; 9 | 10 | export interface RandomQuoteContext { 11 | // The quote's category: 'dev', 'explicit'... 12 | category: string; 13 | } 14 | 15 | @Injectable() 16 | export class QuoteService { 17 | constructor(private httpClient: HttpClient) {} 18 | 19 | getRandomQuote(context: RandomQuoteContext): Observable { 20 | return this.httpClient 21 | .cache() 22 | .get(routes.quote(context)) 23 | .pipe( 24 | map((body: any) => body.value), 25 | catchError(() => of('Error, could not load joke :-(')) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This module imports and re-exports all Angular Material modules for convenience, 3 | * so only 1 module import is needed in your feature modules. 4 | * See https://material.angular.io/guide/getting-started#step-3-import-the-component-modules. 5 | * 6 | * To optimize your production builds, you should only import the components used in your app. 7 | */ 8 | 9 | import { NgModule } from '@angular/core'; 10 | import { 11 | MatAutocompleteModule, 12 | MatButtonModule, 13 | MatButtonToggleModule, 14 | MatCardModule, 15 | MatCheckboxModule, 16 | MatChipsModule, 17 | MatCommonModule, 18 | MatDatepickerModule, 19 | MatDialogModule, 20 | MatDividerModule, 21 | MatExpansionModule, 22 | MatFormFieldModule, 23 | MatGridListModule, 24 | MatIconModule, 25 | MatInputModule, 26 | MatLineModule, 27 | MatListModule, 28 | MatMenuModule, 29 | MatNativeDateModule, 30 | MatOptionModule, 31 | MatPaginatorModule, 32 | MatProgressBarModule, 33 | MatProgressSpinnerModule, 34 | MatPseudoCheckboxModule, 35 | MatRadioModule, 36 | MatRippleModule, 37 | MatSelectModule, 38 | MatSidenavModule, 39 | MatSliderModule, 40 | MatSlideToggleModule, 41 | MatSnackBarModule, 42 | MatSortModule, 43 | MatStepperModule, 44 | MatTableModule, 45 | MatTabsModule, 46 | MatToolbarModule, 47 | MatTooltipModule 48 | } from '@angular/material'; 49 | 50 | @NgModule({ 51 | exports: [ 52 | MatAutocompleteModule, 53 | MatButtonModule, 54 | MatButtonToggleModule, 55 | MatCardModule, 56 | MatCheckboxModule, 57 | MatChipsModule, 58 | MatCommonModule, 59 | MatDatepickerModule, 60 | MatDialogModule, 61 | MatDividerModule, 62 | MatExpansionModule, 63 | MatFormFieldModule, 64 | MatGridListModule, 65 | MatIconModule, 66 | MatInputModule, 67 | MatLineModule, 68 | MatListModule, 69 | MatMenuModule, 70 | MatNativeDateModule, 71 | MatOptionModule, 72 | MatPaginatorModule, 73 | MatProgressBarModule, 74 | MatProgressSpinnerModule, 75 | MatPseudoCheckboxModule, 76 | MatRadioModule, 77 | MatRippleModule, 78 | MatSelectModule, 79 | MatSidenavModule, 80 | MatSlideToggleModule, 81 | MatSliderModule, 82 | MatSnackBarModule, 83 | MatSortModule, 84 | MatStepperModule, 85 | MatTableModule, 86 | MatTabsModule, 87 | MatToolbarModule, 88 | MatTooltipModule 89 | ] 90 | }) 91 | export class MaterialModule {} 92 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shared.module'; 2 | export * from './loader/loader.component'; 3 | -------------------------------------------------------------------------------- /src/app/shared/loader/loader.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ message }} 4 |
5 | -------------------------------------------------------------------------------- /src/app/shared/loader/loader.component.scss: -------------------------------------------------------------------------------- 1 | .mat-progress-spinner { 2 | display: inline-block; 3 | vertical-align: middle; 4 | } 5 | 6 | .message { 7 | margin-left: 0.5em; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/loader/loader.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | import { MaterialModule } from '@app/material.module'; 6 | import { LoaderComponent } from './loader.component'; 7 | 8 | describe('LoaderComponent', () => { 9 | let component: LoaderComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [BrowserAnimationsModule, FlexLayoutModule, MaterialModule], 15 | declarations: [LoaderComponent] 16 | }).compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(LoaderComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should not be visible by default', () => { 26 | // Arrange 27 | const element = fixture.nativeElement; 28 | const div = element.querySelectorAll('div')[0]; 29 | 30 | // Assert 31 | expect(div.getAttribute('hidden')).not.toBeNull(); 32 | }); 33 | 34 | it('should be visible when app is loading', () => { 35 | // Arrange 36 | const element = fixture.nativeElement; 37 | const div = element.querySelectorAll('div')[0]; 38 | 39 | // Act 40 | fixture.componentInstance.isLoading = true; 41 | fixture.detectChanges(); 42 | 43 | // Assert 44 | expect(div.getAttribute('hidden')).toBeNull(); 45 | }); 46 | 47 | it('should not display a message by default', () => { 48 | // Arrange 49 | const element = fixture.nativeElement; 50 | const span = element.querySelectorAll('span')[0]; 51 | 52 | // Assert 53 | expect(span.innerText).toBe(''); 54 | }); 55 | 56 | it('should display specified message', () => { 57 | // Arrange 58 | const element = fixture.nativeElement; 59 | const span = element.querySelectorAll('span')[0]; 60 | 61 | // Act 62 | fixture.componentInstance.message = 'testing'; 63 | fixture.detectChanges(); 64 | 65 | // Assert 66 | expect(span.innerText).toBe('testing'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/app/shared/loader/loader.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loader', 5 | templateUrl: './loader.component.html', 6 | styleUrls: ['./loader.component.scss'] 7 | }) 8 | export class LoaderComponent implements OnInit { 9 | @Input() isLoading = false; 10 | @Input() size = 1; 11 | @Input() message: string; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | import { MaterialModule } from '@app/material.module'; 6 | import { LoaderComponent } from './loader/loader.component'; 7 | import { ConfigToken, DEFAULT_CONFIG } from '../../config'; 8 | 9 | @NgModule({ 10 | imports: [FlexLayoutModule, MaterialModule, CommonModule], 11 | declarations: [LoaderComponent], 12 | exports: [FlexLayoutModule, MaterialModule, LoaderComponent] 13 | }) 14 | export class SharedModule { 15 | static forRoot(): ModuleWithProviders { 16 | return { 17 | ngModule: SharedModule, 18 | providers: [ 19 | { 20 | provide: ConfigToken, 21 | useValue: DEFAULT_CONFIG 22 | } 23 | ] 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | {{ title }} 7 |
8 |
9 | APP_NAME 10 | 14 | 18 |
19 | 20 | 23 | 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | 3 | .navbar { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | z-index: 1; 9 | } 10 | 11 | .brand { 12 | color: mat-color($app-primary, "default-contrast"); 13 | text-decoration: none; 14 | padding-right: 1rem; 15 | } 16 | 17 | .menu-button { 18 | margin-right: 1rem; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import { MaterialModule } from '@app/material.module'; 6 | import { I18nService } from '@app/core'; 7 | import { HeaderComponent } from './header.component'; 8 | 9 | describe('HeaderComponent', () => { 10 | let component: HeaderComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [RouterTestingModule, MaterialModule, TranslateModule.forRoot()], 16 | declarations: [HeaderComponent], 17 | providers: [I18nService] 18 | }).compileComponents(); 19 | })); 20 | 21 | beforeEach(() => { 22 | fixture = TestBed.createComponent(HeaderComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/shell/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Title } from '@angular/platform-browser'; 2 | import { Component, Input, OnInit } from '@angular/core'; 3 | import { MatSidenav } from '@angular/material'; 4 | 5 | import { I18nService } from '@app/core'; 6 | 7 | @Component({ 8 | selector: 'app-header', 9 | templateUrl: './header.component.html', 10 | styleUrls: ['./header.component.scss'] 11 | }) 12 | export class HeaderComponent implements OnInit { 13 | @Input() sidenav: MatSidenav; 14 | 15 | constructor(private titleService: Title, private i18nService: I18nService) {} 16 | 17 | get currentLanguage(): string { 18 | return this.i18nService.language; 19 | } 20 | 21 | get languages(): string[] { 22 | return this.i18nService.supportedLanguages; 23 | } 24 | 25 | get title(): string { 26 | return this.titleService.getTitle(); 27 | } 28 | 29 | ngOnInit() {} 30 | 31 | setLanguage(language: string) { 32 | this.i18nService.language = language; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/theme/theme-variables"; 2 | 3 | :host { 4 | display: flex; 5 | flex: 1; 6 | } 7 | 8 | .mat-sidenav { 9 | min-width: 270px; 10 | max-width: 28%; 11 | } 12 | 13 | .has-border { 14 | border-right: 1px solid rgba(0, 0, 0, 0.12); 15 | } 16 | 17 | .mat-list { 18 | padding-top: 0; 19 | } 20 | 21 | .mat-list-item { 22 | text-decoration: none; 23 | 24 | &:hover { 25 | background: rgba(0, 0, 0, 0.05); 26 | } 27 | } 28 | 29 | .mat-sidenav, 30 | .mat-drawer-content, 31 | // Force style down to child components 32 | // See https://angular.io/guide/component-styles#deep 33 | :host ::ng-deep .mat-drawer-backdrop { 34 | top: 64px; 35 | bottom: 0; 36 | left: 0; 37 | right: 0; 38 | height: auto; 39 | position: absolute; 40 | } 41 | 42 | @media ($mat-xsmall) { 43 | .mat-sidenav, 44 | .mat-drawer-content, 45 | :host ::ng-deep .mat-drawer-backdrop { 46 | top: 56px; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { FlexLayoutModule } from '@angular/flex-layout'; 6 | import { MaterialModule } from '@app/material.module'; 7 | 8 | import { CoreModule } from '@app/core'; 9 | 10 | import { ShellComponent } from './shell.component'; 11 | import { HeaderComponent } from './header/header.component'; 12 | 13 | describe('ShellComponent', () => { 14 | let component: ShellComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(async(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [ 20 | RouterTestingModule, 21 | TranslateModule.forRoot(), 22 | BrowserAnimationsModule, 23 | FlexLayoutModule, 24 | MaterialModule, 25 | CoreModule 26 | ], 27 | declarations: [HeaderComponent, ShellComponent] 28 | }).compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(ShellComponent); 33 | component = fixture.componentInstance; 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/app/shell/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { MediaChange, MediaObserver } from '@angular/flex-layout'; 3 | import { MatSidenav } from '@angular/material'; 4 | import { filter } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-shell', 8 | templateUrl: './shell.component.html', 9 | styleUrls: ['./shell.component.scss'] 10 | }) 11 | export class ShellComponent implements OnInit { 12 | @ViewChild('sidenav') sidenav: MatSidenav; 13 | 14 | constructor(private media: MediaObserver) {} 15 | 16 | ngOnInit() { 17 | // Automatically close side menu on screens > sm breakpoint 18 | this.media.media$ 19 | .pipe(filter((change: MediaChange) => change.mqAlias !== 'xs' && change.mqAlias !== 'sm')) 20 | .subscribe(() => this.sidenav.close()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shell/shell.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { ShellModule } from './shell.module'; 2 | 3 | describe('ShellModule', () => { 4 | let shellModule: ShellModule; 5 | 6 | beforeEach(() => { 7 | shellModule = new ShellModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(shellModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/shell/shell.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { FlexLayoutModule } from '@angular/flex-layout'; 6 | import { MaterialModule } from '@app/material.module'; 7 | 8 | import { ShellComponent } from './shell.component'; 9 | import { HeaderComponent } from './header/header.component'; 10 | 11 | @NgModule({ 12 | imports: [CommonModule, TranslateModule, FlexLayoutModule, MaterialModule, RouterModule], 13 | declarations: [HeaderComponent, ShellComponent] 14 | }) 15 | export class ShellModule {} 16 | -------------------------------------------------------------------------------- /src/app/shell/shell.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ShellComponent } from './shell.component'; 4 | import { Shell } from './shell.service'; 5 | 6 | describe('Shell', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [ShellComponent] 10 | }); 11 | }); 12 | 13 | describe('childRoutes', () => { 14 | it('should create routes as children of shell', () => { 15 | // Prepare 16 | const testRoutes = [{ path: 'test' }]; 17 | 18 | // Act 19 | const result = Shell.childRoutes(testRoutes); 20 | 21 | // Assert 22 | expect(result.path).toBe(''); 23 | expect(result.children).toBe(testRoutes); 24 | expect(result.component).toBe(ShellComponent); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shell/shell.service.ts: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from '@angular/router'; 2 | 3 | import { ShellComponent } from './shell.component'; 4 | 5 | /** 6 | * Provides helper methods to create routes. 7 | */ 8 | export class Shell { 9 | /** 10 | * Creates routes using the shell component and authentication. 11 | * @param routes The routes to add. 12 | * @return The new route using shell as the base. 13 | */ 14 | static childRoutes(routes: Routes): Route { 15 | return { 16 | path: '', 17 | component: ShellComponent, 18 | children: routes, 19 | // Reuse ShellComponent instance when navigating between child views 20 | data: { reuse: true } 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/github-circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/img/portfolio/cabin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/portfolio/cabin.png -------------------------------------------------------------------------------- /src/assets/img/portfolio/cake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/portfolio/cake.png -------------------------------------------------------------------------------- /src/assets/img/portfolio/circus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/portfolio/circus.png -------------------------------------------------------------------------------- /src/assets/img/portfolio/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/portfolio/game.png -------------------------------------------------------------------------------- /src/assets/img/portfolio/safe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/portfolio/safe.png -------------------------------------------------------------------------------- /src/assets/img/portfolio/submarine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/portfolio/submarine.png -------------------------------------------------------------------------------- /src/assets/img/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/img/profile.png -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular Material Freelancer Theme", 3 | "short_name": "Freelancer Theme", 4 | "theme_color": "#18bc9c", 5 | "background_color": "#37474f", 6 | "display": "browser", 7 | "Scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/images/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/images/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/images/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/images/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/images/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/images/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/images/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/images/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ], 51 | "splash_pages": null 52 | } 53 | -------------------------------------------------------------------------------- /src/assets/ngx-rocket-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/ngx-rocket-logo.png -------------------------------------------------------------------------------- /src/assets/pdf/graduate_software_engineer.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/assets/pdf/graduate_software_engineer.pdf -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | last 2 versions 6 | > 0.5% 7 | Firefox ESR 8 | not dead 9 | # For IE 9-11 support, please uncomment the next line and adjust as needed 10 | # IE 9-11 11 | -------------------------------------------------------------------------------- /src/config.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Anthony Nahas 3 | * @since 01.2019 4 | * @version 1.1 5 | */ 6 | export interface Config { 7 | title: string; 8 | description: string; 9 | url: string; 10 | imgUrl?: string; 11 | domain?: string; 12 | about?: { 13 | section1: string; 14 | section2: string; 15 | }; 16 | skills?: string[]; 17 | techStack?: string[]; 18 | projects?: Project[]; 19 | 20 | social?: { 21 | twitter?: string; 22 | facebook?: string; 23 | github?: string; 24 | linkedin?: string; 25 | }; 26 | 27 | address?: { 28 | street?: string; 29 | zip?: string; 30 | city?: string; 31 | state?: string; 32 | country?: string; 33 | }; 34 | } 35 | 36 | export interface Project { 37 | name: string; 38 | description: string; 39 | imageURL?: string; 40 | url?: string; 41 | client?: Client; 42 | } 43 | 44 | export interface Client { 45 | name: string; 46 | imageURL?: string; 47 | } 48 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { Config, Project } from './config.interface'; 2 | import { InjectionToken } from '@angular/core'; 3 | 4 | export { Config } from './config.interface'; 5 | 6 | const exampleProjectDescription: string = 7 | 'Lorem ipsum dolor sit amet, ' + 8 | 'consectetur adipisicing elit. Mollitia neque assumenda ipsam nihil,' + 9 | ' molestias magnam, recusandae quos quis inventore quisquam velit asperiores,' + 10 | ' vitae? Reprehenderit soluta, eos quod consequuntur itaque. Nam.'; 11 | 12 | const projects: Project[] = [ 13 | { 14 | name: 'project name', 15 | description: exampleProjectDescription, 16 | imageURL: 'assets/img/portfolio/cabin.png' 17 | }, 18 | { 19 | name: 'project name', 20 | description: exampleProjectDescription, 21 | imageURL: 'assets/img/portfolio/cake.png' 22 | }, 23 | { 24 | name: 'project name', 25 | description: exampleProjectDescription, 26 | imageURL: 'assets/img/portfolio/circus.png' 27 | }, 28 | { 29 | name: 'project name', 30 | description: exampleProjectDescription, 31 | imageURL: 'assets/img/portfolio/game.png' 32 | }, 33 | { 34 | name: 'project name', 35 | description: exampleProjectDescription, 36 | imageURL: 'assets/img/portfolio/safe.png' 37 | }, 38 | { 39 | name: 'project name', 40 | description: exampleProjectDescription, 41 | imageURL: 'assets/img/portfolio/submarine.png' 42 | } 43 | ]; 44 | 45 | export const DEFAULT_CONFIG: Config = { 46 | title: 'Angular Material Freenlancer Theme', 47 | description: 'Web Developer - Graphic Artist - User Experience Designer', 48 | url: 'https://github.com/angular-material-extensions/freelancer-theme', 49 | domain: 'github.com', 50 | projects: projects, 51 | about: { 52 | section1: 53 | 'This Freelancer Theme is a free material theme created by angular material extensions based on Start' + 54 | ' Bootstrap.The download includes the complete source files including HTML, CSS, and JavaScript as well as ' + 55 | 'optional LESS stylesheets for easy customization.', 56 | section2: 57 | 'Whether you are a student looking to showcase your work, a' + 58 | ' professional looking to attract clients, or a graphic artist looking to share your projects, this template ' + 59 | 'is the perfect starting point!' 60 | }, 61 | social: { 62 | facebook: 'https://www.facebook.com/', 63 | twitter: 'https://twitter.com/ngAnthonyy', 64 | github: 'https://github.com/AnthonyNahas', 65 | linkedin: 'https://www.linkedin.com/in/anthony-nahas-926245119/' 66 | }, 67 | address: { 68 | country: 'Germany', 69 | state: 'Niedersachen', 70 | city: 'Göttingen', 71 | street: 'meine Strasse', 72 | zip: '1234' 73 | } 74 | }; 75 | 76 | export const ConfigToken = new InjectionToken('AngularMaterialFreelancerThemeConfig'); 77 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // `.env.ts` is generated by the `npm run env` command 2 | import env from './.env'; 3 | 4 | export const environment = { 5 | production: true, 6 | version: env.npm_package_version, 7 | defaultLanguage: 'en-US', 8 | supportedLanguages: ['en-US', 'de-DE', 'fr-FR'], 9 | serverUrl: '' 10 | }; 11 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | // `.env.ts` is generated by the `npm run env` command 6 | import env from './.env'; 7 | 8 | export const environment = { 9 | production: false, 10 | version: env.npm_package_version + '-dev', 11 | defaultLanguage: 'en-US', 12 | supportedLanguages: ['en-US', 'de-DE', 'fr-FR'], 13 | serverUrl: '' 14 | }; 15 | 16 | /* 17 | * For easier debugging in development mode, you can import the following file 18 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 19 | * 20 | * This import should be commented out in production mode because it will have a negative impact 21 | * on performance if an error is thrown. 22 | */ 23 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 24 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-material-extensions/freelancer-theme/616e447c3445d17580cdd5de25297f8bf5716089/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @angular-material-extensions/freelancer-theme 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 80 | 81 | 87 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 5 | const path = require('path'); 6 | 7 | module.exports = function(config) { 8 | config.set({ 9 | basePath: '..', 10 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 11 | plugins: [ 12 | require('karma-jasmine'), 13 | require('karma-chrome-launcher'), 14 | require('karma-junit-reporter'), 15 | require('karma-coverage-istanbul-reporter'), 16 | require('@angular-devkit/build-angular/plugins/karma') 17 | ], 18 | client: { 19 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 20 | captureConsole: Boolean(process.env.KARMA_ENABLE_CONSOLE) 21 | }, 22 | junitReporter: { 23 | outputDir: path.join(__dirname, '../reports/junit/'), 24 | outputFile: 'TESTS-xunit.xml', 25 | useBrowserName: false, 26 | suite: '' // Will become the package name attribute in xml testsuite element 27 | }, 28 | coverageIstanbulReporter: { 29 | reports: ['html', 'lcovonly', 'text-summary'], 30 | dir: path.join(__dirname, '../reports/coverage'), 31 | fixWebpackSourcePaths: true 32 | }, 33 | angularCli: { 34 | environment: 'dev' 35 | }, 36 | reporters: ['progress', 'junit'], 37 | port: 9876, 38 | colors: true, 39 | // Level of logging, can be: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 40 | logLevel: config.LOG_INFO, 41 | autoWatch: true, 42 | browsers: ['ChromeHeadless'], 43 | singleRun: false 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point of global application style. 3 | * Component-specific style should not go here and be included directly as part of the components. 4 | */ 5 | // Theme variables, must be included before the libraries to allow overriding defaults 6 | @import "theme/theme-variables"; 7 | // 3rd party libraries 8 | //@import "~material-design-icons-iconfont/dist/fonts/material-icons.css"; 9 | // Angular Material custom theme 10 | // The mixins below must be included once so we separated them from the variables 11 | @import "~@angular/material/theming"; 12 | 13 | // Include the common styles for Angular Material. We include this here so that you only 14 | // have to load a single css file for Angular Material in your app. 15 | // Be sure that you only ever include this mixin once! 16 | @include mat-core(); 17 | 18 | // Include theme styles for core and each component used in your app. 19 | // Alternatively, you can import and @include the theme mixins for each component 20 | // that you are using. 21 | @include angular-material-theme($app-theme); 22 | 23 | // Theme customization 24 | @import "theme/theme"; 25 | 26 | $facebook: #385899; 27 | $twitter: #1da1f2; 28 | $github: #b21a32; 29 | 30 | .container { 31 | text-align: center; 32 | padding: 1em; 33 | } 34 | 35 | .fill-remaining { 36 | flex: 1 1 auto; 37 | } 38 | 39 | .text-uppercase { 40 | text-transform: uppercase; 41 | } 42 | 43 | .text-center { 44 | text-align: center; 45 | } 46 | 47 | .text-white { 48 | color: #fff; 49 | } 50 | 51 | .bg-primary { 52 | background-color: #18bc9c; 53 | } 54 | 55 | .bg-secondary { 56 | background-color: #2c3e50; 57 | } 58 | 59 | .mb-2, 60 | .my-2 { 61 | margin-bottom: 0.5rem; 62 | } 63 | 64 | .mb-4, 65 | .my-4 { 66 | margin-bottom: 1.5rem; 67 | } 68 | 69 | .m-4 { 70 | margin: 1.5rem; 71 | } 72 | 73 | .img-fluid { 74 | max-height: 400px; 75 | max-width: 100%; 76 | height: auto; 77 | } 78 | 79 | @media only screen and (max-width: 599px) { 80 | .img-project { 81 | max-height: 200px; 82 | } 83 | } 84 | 85 | @media only (min-width: 599px) and (max-width: 1279px) { 86 | .img-project { 87 | max-height: 300px; 88 | } 89 | } 90 | 91 | @media only (min-width: 1279px) { 92 | .img-project { 93 | max-height: 400px; 94 | } 95 | } 96 | 97 | .margin-auto { 98 | margin-left: auto; 99 | margin-right: auto; 100 | } 101 | 102 | .project { 103 | margin: 0.5rem; 104 | cursor: pointer; 105 | } 106 | 107 | .full-width { 108 | width: 100%; 109 | } 110 | 111 | .contact-form { 112 | min-width: 150px; 113 | max-width: 500px; 114 | width: 100%; 115 | } 116 | 117 | .footer { 118 | padding-top: 5rem; 119 | padding-bottom: 5rem; 120 | background-color: #2c3e50; 121 | color: #fff; 122 | 123 | .mat-fab { 124 | margin-right: 0.25rem; 125 | margin-left: 0.25rem; 126 | margin-bottom: 0.5rem; 127 | } 128 | } 129 | 130 | .copyright { 131 | background-color: #1a252f; 132 | text-align: center; 133 | color: #fff; 134 | padding-bottom: 1.5rem; 135 | padding-top: 1.5rem; 136 | } 137 | 138 | .lead { 139 | font-size: 1.25rem; 140 | font-weight: 300; 141 | } 142 | 143 | .mat-divider { 144 | left: auto; 145 | max-width: 15rem; 146 | //padding: 0; 147 | text-align: center; 148 | border: none #fff; 149 | border-top: 0.25rem solid; 150 | overflow: hidden; 151 | border-style: inset; 152 | border-width: 1px; 153 | box-sizing: border-box; 154 | } 155 | 156 | .mat-icon-star { 157 | .grey { 158 | color: #37474f; 159 | } 160 | 161 | .white { 162 | color: #37474f; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | export { AppServerModule } from './app/app.server.module'; 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Entry point of the application. 3 | * Only platform bootstrapping code should be here. 4 | * For app-specific initialization, use `app/app.component.ts`. 5 | */ 6 | 7 | import 'hammerjs'; 8 | import { enableProdMode } from '@angular/core'; 9 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 10 | 11 | import { AppModule } from '@app/app.module'; 12 | import { environment } from '@env/environment'; 13 | 14 | const bootstrap = () => 15 | platformBrowserDynamic() 16 | .bootstrapModule(AppModule) 17 | .catch(err => console.log(err)); 18 | 19 | if (environment.production) { 20 | enableProdMode(); 21 | } 22 | 23 | bootstrap(); 24 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freelancer-theme", 3 | "short_name": "freelancer-theme", 4 | "theme_color": "#488aff", 5 | "background_color": "#488aff", 6 | "scope": "/", 7 | "start_url": "/", 8 | "display": "standalone", 9 | "icons": [ 10 | { 11 | "src": "assets/ngx-rocket-logo.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. */ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** 38 | * If the application will be indexed by Google Search, the following is required. 39 | * Googlebot uses a renderer based on Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | */ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | /** 51 | * Web Animations `@angular/platform-browser/animations` 52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 54 | */ 55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 56 | 57 | /** 58 | * By default, zone.js will patch all possible macroTask and DomEvents 59 | * user can disable parts of macroTask/DomEvents patch by setting following flags 60 | */ 61 | 62 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 63 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 64 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 65 | 66 | /* 67 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 68 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 69 | */ 70 | // (window as any).__Zone_enable_cross_context_check = true; 71 | 72 | /*************************************************************************************************** 73 | * Zone JS is required by default for Angular itself. 74 | */ 75 | import 'zone.js/dist/zone'; // Included with Angular CLI. 76 | 77 | /*************************************************************************************************** 78 | * APPLICATION IMPORTS 79 | */ 80 | -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Prepare environment for unit tests. 3 | * This file is required by karma.conf.js and loads recursively all the .spec and framework files. 4 | */ 5 | 6 | import 'zone.js/dist/zone-testing'; 7 | import { getTestBed } from '@angular/core/testing'; 8 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 14 | // Then we find all the tests. 15 | const context = require.context('./', true, /\.spec\.ts$/); 16 | // And load the modules. 17 | context.keys().map(context); 18 | -------------------------------------------------------------------------------- /src/theme/theme-variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Application global variables. 3 | */ 4 | // Angular Material custom theme 5 | // See https://material.angular.io/guide/theming for more details. 6 | // 7 | // You can also read https://medium.com/@tomastrajan/the-complete-guide-to-angular-material-themes-4d165a9d24d1 8 | // for more insight about Angular Material theming. 9 | @import "~@angular/material/theming"; 10 | 11 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 12 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 13 | // hue. 14 | $app-primary: mat-palette($mat-blue-gray, 800); 15 | $app-accent: mat-palette($mat-teal, A700); 16 | 17 | // The warn palette is optional (defaults to red). 18 | $app-warn: mat-palette($mat-red); 19 | 20 | // Create the theme object (a Sass map containing all of the palettes). 21 | $app-theme: mat-light-theme($app-primary, $app-accent, $app-warn); 22 | -------------------------------------------------------------------------------- /src/theme/theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Global application theme. 3 | * Framework overrides and customization goes here. 4 | */ 5 | 6 | // stylelint-disable-next-line selector-max-universal 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | html, 12 | body { 13 | display: flex; 14 | flex-direction: column; 15 | margin: 0; 16 | height: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /src/translations/de-DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_NAME": "Ihre TItle", 3 | "ABOUT": "Über", 4 | "Version": "Version", 5 | "PORTFOLIO": "Portfolio", 6 | "ADDRESS": "Adresse", 7 | "CONTACT": { 8 | "SELF": "Kontakt", 9 | "NAME": "Name", 10 | "EMAIL": "Email", 11 | "PHONE_NUMBER": "Telefonnummer", 12 | "MESSAGE": "Nachricht", 13 | "SEND": "Senden" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/translations/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_NAME": "YOUR TITLE", 3 | "ABOUT": "About", 4 | "Version": "Version", 5 | "PORTFOLIO": "Portfolio", 6 | "ADDRESS": "Address", 7 | "CONTACT": { 8 | "SELF": "Contact", 9 | "NAME": "Name", 10 | "EMAIL": "Email", 11 | "PHONE_NUMBER": "Mobile Nummer", 12 | "MESSAGE": "Message", 13 | "SEND": "Send" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/translations/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP_NAME": "VOTRE TITRE", 3 | "ABOUT": "A propos", 4 | "Version": "Version", 5 | "PORTFOLIO": "Portefeuille", 6 | "ADDRESS": "Adresse", 7 | "CONTACT": { 8 | "SELF": "Contact", 9 | "NAME": "Nom", 10 | "EMAIL": "Email", 11 | "PHONE_NUMBER": "Numéro de téléphone", 12 | "MESSAGE": "Message", 13 | "SEND": "Envoyer" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": ["test.ts", "**/*.spec.ts", "**/*.mock.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [] 8 | }, 9 | "exclude": ["testing/*", "test.ts", "setupJest.ts", "jestGlobalMocks.ts", "**/*.spec.ts"], 10 | "angularCompilerOptions": { 11 | "entryModule": "app/app.server.module#AppServerModule" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["jasmine", "node"] 9 | }, 10 | "files": ["test.ts", "polyfills.ts"], 11 | "include": ["**/*.spec.ts", "**/*.mock.ts", "**/*.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Extra typings definitions 3 | */ 4 | 5 | // Allow .json files imports 6 | declare module '*.json'; 7 | 8 | // SystemJS module definition 9 | declare var module: NodeModule; 10 | interface NodeModule { 11 | id: string; 12 | } 13 | -------------------------------------------------------------------------------- /static.paths.ts: -------------------------------------------------------------------------------- 1 | export const ROUTES = ['/home', '/getting-started/']; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "noImplicitAny": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "target": "es5", 14 | "typeRoots": ["node_modules/@types"], 15 | "lib": ["es2018", "dom"], 16 | "baseUrl": "src", 17 | "paths": { 18 | "@app/*": ["app/*"], 19 | "@env/*": ["environments/*"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-prettier"], 3 | "rulesDirectory": ["node_modules/codelyzer"], 4 | "rules": { 5 | "arrow-return-shorthand": true, 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [true, "check-space"], 9 | "curly": true, 10 | "deprecation": { 11 | "severity": "warn" 12 | }, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs/Rx"], 16 | "import-spacing": true, 17 | "indent": [true, "spaces"], 18 | "interface-over-type-literal": true, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [true, 120], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "public-static-field", 28 | "protected-static-field", 29 | "private-static-field", 30 | "public-instance-field", 31 | "protected-instance-field", 32 | "private-instance-field", 33 | "public-static-method", 34 | "protected-static-method", 35 | "private-static-method", 36 | "constructor", 37 | "public-instance-method", 38 | "protected-instance-method", 39 | "private-instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [true, "debug", "time", "timeEnd", "trace"], 46 | "no-construct": true, 47 | "no-debugger": true, 48 | "no-duplicate-variable": [true, "check-parameters"], 49 | "no-duplicate-super": true, 50 | "no-empty": false, 51 | "no-empty-interface": true, 52 | "no-eval": true, 53 | "no-inferrable-types": [true, "ignore-params"], 54 | "no-misused-new": true, 55 | "no-non-null-assertion": true, 56 | "no-redundant-jsdoc": true, 57 | "no-shadowed-variable": true, 58 | "no-string-literal": false, 59 | "no-string-throw": true, 60 | "no-switch-case-fall-through": true, 61 | "no-trailing-whitespace": true, 62 | "no-unnecessary-initializer": true, 63 | "no-unused-expression": true, 64 | "no-use-before-declare": true, 65 | "no-var-keyword": true, 66 | "object-literal-sort-keys": false, 67 | "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace"], 68 | "prefer-const": true, 69 | "quotemark": [true, "single"], 70 | "radix": true, 71 | "semicolon": [true, "always"], 72 | "triple-equals": [true, "allow-null-check"], 73 | "typedef": [true, "parameter", "property-declaration"], 74 | "typedef-whitespace": [ 75 | true, 76 | { 77 | "call-signature": "nospace", 78 | "index-signature": "nospace", 79 | "parameter": "nospace", 80 | "property-declaration": "nospace", 81 | "variable-declaration": "nospace" 82 | } 83 | ], 84 | "unified-signatures": true, 85 | "variable-name": false, 86 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"], 87 | "directive-selector": [true, "attribute", "app", "camelCase"], 88 | "component-selector": [true, "element", "app", "kebab-case"], 89 | "no-output-on-prefix": true, 90 | "use-input-property-decorator": true, 91 | "use-output-property-decorator": true, 92 | "use-host-property-decorator": true, 93 | "no-input-rename": true, 94 | "no-output-rename": true, 95 | "use-life-cycle-interface": true, 96 | "use-pipe-transform-interface": true, 97 | "component-class-suffix": true, 98 | "directive-class-suffix": true 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | // Work around for https://github.com/angular/angular-cli/issues/7200 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | mode: 'none', 8 | entry: { 9 | // This is our Express server for Dynamic universal 10 | server: './server.ts', 11 | // This is an example of Static prerendering (generative) 12 | prerender: './prerender.ts' 13 | }, 14 | target: 'node', 15 | resolve: { extensions: ['.ts', '.js'] }, 16 | // Make sure we include all node_modules etc 17 | externals: [/node_modules/], 18 | output: { 19 | // Puts the output at the root of the dist folder 20 | path: path.join(__dirname, 'dist'), 21 | filename: '[name].js' 22 | }, 23 | module: { 24 | rules: [{ test: /\.ts$/, loader: 'ts-loader' }] 25 | }, 26 | plugins: [ 27 | new webpack.ContextReplacementPlugin( 28 | // fixes WARNING Critical dependency: the request of a dependency is an expression 29 | /(.+)?angular(\\|\/)core(.+)?/, 30 | path.join(__dirname, 'src'), // location of your src 31 | {} // a map of your routes 32 | ), 33 | new webpack.ContextReplacementPlugin( 34 | // fixes WARNING Critical dependency: the request of a dependency is an expression 35 | /(.+)?express(\\|\/)(.+)?/, 36 | path.join(__dirname, 'src'), 37 | {} 38 | ) 39 | ] 40 | }; 41 | --------------------------------------------------------------------------------