├── .browserslistrc
├── .commitlintrc.js
├── .cspell.json
├── .czrc
├── .dockerignore
├── .editorconfig
├── .env
├── .eslintignore
├── .eslintrc.js
├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── .htmlhintrc
├── .husky
├── commit-msg
└── pre-commit
├── .lintstagedrc.js
├── .ncurc.js
├── .npmignore
├── .npmrc
├── .prettierignore
├── .prettierrc.js
├── .stylelintignore
├── .tool-versions
├── .versionrc.js
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── TODO.md
├── angular.json
├── cypress.config.ts
├── cypress
├── coverage.webpack.js
├── e2e
│ ├── app.cy.ts
│ └── app.e2e.ts
├── fixtures
│ └── example.json
├── support
│ ├── commands.ts
│ └── e2e.ts
└── tsconfig.json
├── docker
├── Dockerfile
├── docker-compose.csr.yml
├── docker-compose.ssr.yml
├── nginx
│ └── nginx.conf
└── pm2
│ └── process.json
├── jest.config.js
├── ngsw-config.json
├── nyc.config.js
├── package-lock.json
├── package.json
├── routes.txt
├── server.ts
├── src
├── app
│ ├── app-routing.module.ts
│ ├── app.browser.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── app.server.module.ts
│ ├── core
│ │ ├── components
│ │ │ ├── content
│ │ │ │ ├── content.component.html
│ │ │ │ ├── content.component.scss
│ │ │ │ ├── content.component.spec.ts
│ │ │ │ └── content.component.ts
│ │ │ ├── footer
│ │ │ │ ├── footer.component.html
│ │ │ │ ├── footer.component.scss
│ │ │ │ └── footer.component.ts
│ │ │ ├── header
│ │ │ │ ├── header.component.html
│ │ │ │ ├── header.component.scss
│ │ │ │ └── header.component.ts
│ │ │ ├── index.ts
│ │ │ └── navbar
│ │ │ │ ├── navbar.component.html
│ │ │ │ ├── navbar.component.scss
│ │ │ │ └── navbar.component.ts
│ │ ├── constants
│ │ │ ├── index.ts
│ │ │ ├── languages.ts
│ │ │ └── menu-entries.ts
│ │ ├── core.module.ts
│ │ ├── enums
│ │ │ ├── cookie.ts
│ │ │ ├── header.ts
│ │ │ ├── index.ts
│ │ │ └── transfer-state-key.ts
│ │ ├── guards
│ │ │ ├── ensure-module-loaded-once.guard.ts
│ │ │ ├── index.ts
│ │ │ └── localize-route.guard.ts
│ │ ├── handlers
│ │ │ ├── global-error.handler.ts
│ │ │ └── index.ts
│ │ ├── interceptors
│ │ │ ├── http-error.interceptor.ts
│ │ │ └── index.ts
│ │ ├── loaders
│ │ │ ├── i18n-browser.loader.ts
│ │ │ └── i18n-server.loader.ts
│ │ ├── models
│ │ │ ├── error-event.ts
│ │ │ └── index.ts
│ │ ├── resolvers
│ │ │ ├── index.ts
│ │ │ └── language.resolver.ts
│ │ ├── services
│ │ │ ├── app-bus.service.ts
│ │ │ ├── app-initializer.service.ts
│ │ │ ├── app-updater.service.ts
│ │ │ ├── cookie.service.ts
│ │ │ ├── i18n.service.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.service.spec.ts
│ │ │ ├── logger.service.ts
│ │ │ └── platform.service.ts
│ │ ├── strategies
│ │ │ ├── custom-lazyload-image.strategy.ts
│ │ │ ├── custom-page-title.strategy.ts
│ │ │ ├── custom-route-preload.strategy.ts
│ │ │ ├── custom-route-reuse.strategy.ts
│ │ │ ├── index.ts
│ │ │ └── network-aware-route-preload.strategy.ts
│ │ ├── tokens
│ │ │ └── app-name.ts
│ │ └── utils
│ │ │ └── index.ts
│ ├── features
│ │ ├── home
│ │ │ ├── components
│ │ │ │ └── index.ts
│ │ │ ├── home-routing.module.ts
│ │ │ ├── home.module.ts
│ │ │ ├── models
│ │ │ │ └── index.ts
│ │ │ ├── pages
│ │ │ │ ├── index.ts
│ │ │ │ └── welcome
│ │ │ │ │ ├── welcome.component.html
│ │ │ │ │ ├── welcome.component.scss
│ │ │ │ │ └── welcome.component.ts
│ │ │ └── services
│ │ │ │ └── index.ts
│ │ └── jokes
│ │ │ ├── components
│ │ │ └── index.ts
│ │ │ ├── jokes-routing.module.ts
│ │ │ ├── jokes.module.ts
│ │ │ ├── models
│ │ │ ├── chuck-norris-joke.model.ts
│ │ │ └── index.ts
│ │ │ ├── pages
│ │ │ ├── chuck-norris
│ │ │ │ ├── chuck-norris.component.html
│ │ │ │ ├── chuck-norris.component.scss
│ │ │ │ └── chuck-norris.component.ts
│ │ │ └── index.ts
│ │ │ └── services
│ │ │ ├── chuck-norris.service.ts
│ │ │ └── index.ts
│ └── shared
│ │ ├── components
│ │ ├── error
│ │ │ ├── error.component.html
│ │ │ ├── error.component.scss
│ │ │ └── error.component.ts
│ │ └── index.ts
│ │ ├── directives
│ │ └── index.ts
│ │ ├── modules
│ │ └── icons
│ │ │ ├── github-icon
│ │ │ ├── github-icon.component.html
│ │ │ ├── github-icon.component.scss
│ │ │ └── github-icon.component.ts
│ │ │ ├── happy-icon
│ │ │ ├── happy-icon.component.html
│ │ │ ├── happy-icon.component.scss
│ │ │ └── happy-icon.component.ts
│ │ │ ├── heart-icon
│ │ │ ├── heart-icon.component.html
│ │ │ ├── heart-icon.component.scss
│ │ │ └── heart-icon.component.ts
│ │ │ └── icons.module.ts
│ │ ├── pipes
│ │ ├── index.ts
│ │ └── localize-route.pipe.ts
│ │ ├── services
│ │ ├── browser.service.ts
│ │ ├── index.ts
│ │ ├── seo.service.ts
│ │ ├── server.service.ts
│ │ └── storage.service.ts
│ │ └── shared.module.ts
├── assets
│ ├── i18n
│ │ ├── en.json
│ │ └── es.json
│ ├── 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
│ └── images
│ │ ├── background.svg
│ │ ├── logo.png
│ │ └── logo.svg
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── favicon.png
├── index.html
├── jest.mocks.ts
├── jest.setup.ts
├── main.browser.ts
├── main.server.ts
├── manifest.webmanifest
├── polyfills.ts
├── robots.txt
├── styles.scss
├── styles
│ ├── abstracts
│ │ ├── _functions.scss
│ │ ├── _mixins.scss
│ │ └── _variables.scss
│ ├── base
│ │ ├── _base.scss
│ │ ├── _fonts.scss
│ │ ├── _helpers.scss
│ │ └── _typography.scss
│ ├── components
│ │ └── _button.scss
│ ├── layout
│ │ ├── _content.scss
│ │ ├── _footer.scss
│ │ ├── _header.scss
│ │ ├── _navbar.scss
│ │ └── _root.scss
│ ├── main.scss
│ └── vendors
│ │ ├── _google.scss
│ │ └── _tailwind.scss
└── types
│ ├── figlet
│ └── importable-fonts.d.ts
│ └── globals.d.ts
├── stylelint.config.js
├── tailwind.config.js
├── transloco.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.server.json
└── tsconfig.spec.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 |
--------------------------------------------------------------------------------
/.commitlintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional']
3 | };
4 |
--------------------------------------------------------------------------------
/.cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2",
3 | "language": "en",
4 | "import": [
5 | "@cspell/dict-typescript",
6 | "@cspell/dict-software-terms",
7 | "@cspell/dict-node",
8 | "@cspell/dict-en_us",
9 | "@cspell/dict-en-gb",
10 | "@cspell/dict-npm",
11 | "@cspell/dict-html",
12 | "@cspell/dict-companies",
13 | "@cspell/dict-filetypes",
14 | "@cspell/dict-bash",
15 | "@cspell/dict-lorem-ipsum/cspell-ext.json",
16 | "@cspell/dict-es-es/cspell-ext.json"
17 | ],
18 | "allowCompoundWords": true,
19 | "flagWords": [],
20 | "ignorePaths": [
21 | ".eslintcache",
22 | ".stylelintcache",
23 | ".vscode/**",
24 | "dist/**",
25 | "node_modules/**",
26 | "package-lock.json",
27 | "package.json",
28 | "tsconfig.json",
29 | "tsconfig.build.json",
30 | "yarn.lock",
31 | "**/coverage/**",
32 | "**/node_modules/**",
33 | "**/dist/**",
34 | "**/fixtures/**",
35 | "**/public/**",
36 | "CHANGELOG.md",
37 | "**/changelog.mdx"
38 | ],
39 | "dictionaries": [
40 | "typescript",
41 | "softwareTerms",
42 | "node",
43 | "en_us",
44 | "en-gb",
45 | "npm",
46 | "html",
47 | "companies",
48 | "misc",
49 | "filetypes",
50 | "bash",
51 | "lorem-ipsum",
52 | "es-es"
53 | ],
54 | "words": [
55 | "apng",
56 | "avif",
57 | "Bienvenidx",
58 | "borjapazr",
59 | "browserslistrc",
60 | "commitlintrc",
61 | "czrc",
62 | "endent",
63 | "healthz",
64 | "htmlhintrc",
65 | "iisnode",
66 | "Jang",
67 | "joelwmale",
68 | "lcov",
69 | "lintstagedrc",
70 | "ncurc",
71 | "Neue",
72 | "ngfactory",
73 | "ngneat",
74 | "ngstyle",
75 | "ngsw",
76 | "nguniversal",
77 | "opsz",
78 | "OWASP",
79 | "robotstxt",
80 | "skyux",
81 | "sonarjs",
82 | "styl",
83 | "Suvorov",
84 | "Titillium",
85 | "Todoist",
86 | "Transloco",
87 | "versionrc",
88 | "webp",
89 | "wght"
90 | ]
91 | }
92 |
--------------------------------------------------------------------------------
/.czrc:
--------------------------------------------------------------------------------
1 | {
2 | "path": "cz-conventional-changelog"
3 | }
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .commitlintrc*
2 | .dockerignore
3 | .DS_Store
4 | .editorconfig
5 | .env*
6 | .eslintcache
7 | .stylelintcache
8 | .eslintrc*
9 | .git
10 | .prettierrc*
11 | .vscode
12 | *.log
13 | *docker-compose*
14 | *Dockerfile*
15 | .docker
16 | coverage
17 | data
18 | dist
19 | LICENSE
20 | logs
21 | node_modules
22 | npm-debug.log
23 | README.md
24 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.{js,ts}]
12 | quote_type = single
13 |
14 | [*.json]
15 | insert_final_newline = ignore
16 |
17 | [{Makefile,**.mk}]
18 | indent_style = tab
19 |
20 | [*.md]
21 | max_line_length = off
22 | trim_trailing_whitespace = false
23 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | ## App configuration ##
2 | APP_NAME=angular-skeleton
3 | PORT=5000
4 | ISR_TOKEN=angular-skeleton
5 |
6 | ## Docker configuration ##
7 | # App configuration #
8 | EXTERNAL_PORT=5000
9 |
10 | # Container configuration
11 | TZ=Europe/Madrid
12 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Directories
2 | .yarn/
3 | .docker/
4 | **/.coverage_artifacts
5 | **/.coverage_cache
6 | **/.coverage_contracts
7 | **/artifacts
8 | **/build
9 | **/cache
10 | **/coverage
11 | **/dist
12 | **/logs
13 | **/node_modules
14 | **/types
15 |
16 | # Files
17 | .commitlintrc*
18 | .eslintcache
19 | .stylelintcache
20 | .eslintrc*
21 | .pnp.*
22 | .prettierrc*
23 | *.env
24 | *.log
25 | coverage.json
26 | npm-debug.log*
27 | package-lock.json
28 | yarn.lock
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*
2 | * simple-import-sort default grouping, but with type imports last as a separate
3 | * group, sorting that group like non-type imports are grouped.
4 | */
5 | const importGroups = [
6 | // Side effect imports.
7 | ['^\\u0000'],
8 | // Packages.
9 | // Things that start with a letter (or digit or underscore), or `@` followed by a letter.
10 | ['^@?\\w'],
11 | // Absolute imports and other imports such as Vue-style `@/foo`.
12 | // Anything not matched in another group.
13 | ['^'],
14 | // Relative imports.
15 | // Anything that starts with a dot
16 | ['^\\.'],
17 | // Typings
18 | ['^@?\\w.*\\u0000$', '^[^.].*\\u0000$', '^\\..*\\u0000$']
19 | ];
20 |
21 | /*
22 | * Configuration for simple-import-sort plugin to detect
23 | * the different namespaces defined in the application.
24 | * This matches the "paths" property of the tsconfig.json file.
25 | */
26 | const { compilerOptions } = require('get-tsconfig').getTsconfig('./tsconfig.json')['config'];
27 | if ('paths' in compilerOptions) {
28 | const namespaces = Object.keys(compilerOptions.paths).map(path => path.replace('/*', ''));
29 | if (namespaces && namespaces.length > 0) {
30 | // Anything that is defined in tsconfig.json with a little trick in order to resolve paths
31 | const pathAliasRegex = [`^(${namespaces.join('|')})(/.*|$)`];
32 | importGroups.splice(2, 0, pathAliasRegex);
33 | }
34 | }
35 |
36 | /*
37 | * Although many of the extended configurations already automatically
38 | * import the plugins, we have chosen to add them explicitly in case
39 | * the recommended configurations are dispensed with in the future.
40 | * In this way the rules could be added directly in the "rules" section.
41 | */
42 | module.exports = {
43 | root: true,
44 | ignorePatterns: ['projects/**/*'],
45 | env: {
46 | node: true,
47 | jest: true,
48 | 'jest/globals': true,
49 | 'cypress/globals': true
50 | },
51 | parserOptions: {
52 | ecmaVersion: 12,
53 | sourceType: 'module'
54 | },
55 | overrides: [
56 | {
57 | files: '*.js',
58 | extends: ['eslint:recommended']
59 | },
60 | {
61 | files: ['*.ts'],
62 | parser: '@typescript-eslint/parser',
63 | parserOptions: {
64 | project: ['./tsconfig.json', './tsconfig.app.json', './tsconfig.server.json', './tsconfig.spec.json'],
65 | createDefaultProgram: true
66 | },
67 | extends: [
68 | 'plugin:@angular-eslint/recommended',
69 | 'plugin:@angular-eslint/recommended--extra',
70 | 'plugin:@angular-eslint/template/process-inline-templates',
71 | 'plugin:eslint-comments/recommended',
72 | 'plugin:sonarjs/recommended',
73 | 'plugin:import/recommended',
74 | 'plugin:import/typescript',
75 | 'plugin:prettier/recommended'
76 | ],
77 | plugins: [
78 | '@typescript-eslint',
79 | 'prefer-arrow',
80 | 'eslint-comments',
81 | 'sonarjs',
82 | 'import',
83 | 'prettier',
84 | 'simple-import-sort',
85 | 'unused-imports',
86 | 'deprecation'
87 | ],
88 | settings: {
89 | // Define import resolver for import plugin
90 | 'import/resolver': {
91 | typescript: {
92 | alwaysTryTypes: true
93 | }
94 | }
95 | },
96 | rules: {
97 | // For faster development
98 | 'no-process-exit': 'off',
99 | 'no-useless-constructor': 'off',
100 | 'class-methods-use-this': 'off',
101 | '@typescript-eslint/no-explicit-any': 'off',
102 | '@typescript-eslint/explicit-function-return-type': 'off',
103 | '@typescript-eslint/no-non-null-assertion': 'off',
104 | 'sonarjs/cognitive-complexity': ['error', 18],
105 |
106 | // Angular specific
107 | '@angular-eslint/directive-selector': [
108 | 'error',
109 | {
110 | type: 'attribute',
111 | prefix: 'app',
112 | style: 'camelCase'
113 | }
114 | ],
115 | '@angular-eslint/component-selector': [
116 | 'error',
117 | {
118 | type: 'element',
119 | prefix: 'app',
120 | style: 'kebab-case'
121 | }
122 | ],
123 |
124 | // Import and order style
125 | 'simple-import-sort/imports': [
126 | 'error',
127 | {
128 | groups: importGroups
129 | }
130 | ],
131 | 'no-restricted-imports': [
132 | 'error',
133 | {
134 | patterns: [
135 | {
136 | group: ['../*', './../*'],
137 | message: 'For imports of parent elements use better path aliases. For example, @domain/shared.'
138 | }
139 | ]
140 | }
141 | ],
142 | 'import/no-named-as-default': 'off',
143 | 'import/no-named-as-default-member': 'off',
144 | 'simple-import-sort/exports': 'error',
145 | 'import/prefer-default-export': 'off',
146 | 'import/no-default-export': 'off',
147 | 'import/first': 'error',
148 | 'import/newline-after-import': 'error',
149 | 'import/no-duplicates': 'error',
150 | 'import/no-deprecated': 'error',
151 | 'import/group-exports': 'error',
152 | 'import/exports-last': 'error',
153 | 'padding-line-between-statements': [
154 | 'error',
155 | { blankLine: 'always', prev: '*', next: 'export' },
156 | { blankLine: 'any', prev: 'export', next: 'export' }
157 | ],
158 | quotes: [
159 | 'error',
160 | 'single',
161 | {
162 | allowTemplateLiterals: true
163 | }
164 | ],
165 | '@typescript-eslint/member-ordering': 'error',
166 | '@typescript-eslint/no-unused-vars': 'off',
167 | 'no-unused-vars': 'off',
168 | 'unused-imports/no-unused-imports': 'error',
169 | 'unused-imports/no-unused-vars': [
170 | 'error',
171 | { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }
172 | ],
173 |
174 | // General rules
175 | 'deprecation/deprecation': 'warn',
176 | 'eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
177 | 'lines-between-class-members': 'off',
178 | '@typescript-eslint/lines-between-class-members': 'error',
179 | 'prefer-arrow/prefer-arrow-functions': [
180 | 'warn',
181 | {
182 | disallowPrototype: true,
183 | singleReturnOnly: false,
184 | classPropertiesAllowed: false
185 | }
186 | ]
187 | },
188 | overrides: [
189 | {
190 | files: ['*.unit.ts', '*.int.ts', '*.spec.ts', '*.test.ts'],
191 | env: {
192 | jest: true,
193 | 'jest/globals': true
194 | },
195 | extends: ['plugin:jest/recommended', 'plugin:jest/style'],
196 | plugins: ['jest'],
197 | rules: {
198 | 'jest/expect-expect': ['error', { assertFunctionNames: ['expect', 'request.**.expect'] }]
199 | }
200 | },
201 | {
202 | files: ['*.e2e.ts', '*.cy.ts'],
203 | env: {
204 | 'cypress/globals': true
205 | },
206 | parserOptions: {
207 | project: './cypress/tsconfig.json'
208 | },
209 | extends: ['plugin:cypress/recommended'],
210 | plugins: ['cypress'],
211 | rules: {}
212 | }
213 | ]
214 | },
215 | {
216 | files: ['*.html'],
217 | extends: ['plugin:@angular-eslint/template/recommended'],
218 | rules: {}
219 | },
220 | {
221 | files: ['*.html'],
222 | excludedFiles: ['*inline-template-*.component.html'],
223 | extends: ['plugin:prettier/recommended'],
224 | rules: {
225 | 'prettier/prettier': ['error', { parser: 'angular' }]
226 | }
227 | }
228 | ]
229 | };
230 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @borjapazr
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: 'weekly'
7 | day: 'friday'
8 | commit-message:
9 | prefix: 'npm'
10 | include: 'scope'
11 | labels:
12 | - 'npm'
13 | - 'dependencies'
14 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CD
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | release:
13 | name: 🐙 Generate new release
14 | runs-on: ubuntu-latest
15 | if: startsWith(github.ref, 'refs/tags/')
16 |
17 | steps:
18 | - name: ⬇️ Checkout project
19 | uses: actions/checkout@v3
20 |
21 | - name: 📋 Build Changelog
22 | run: npx extract-changelog-release > RELEASE_BODY.md
23 |
24 | - name: 🍻 Build and generate new release
25 | uses: softprops/action-gh-release@v1
26 | with:
27 | body_path: RELEASE_BODY.md
28 | token: ${{ secrets.GITHUB_TOKEN }}
29 |
30 | distribute:
31 | name: 🛩️ Deliver project
32 | runs-on: ubuntu-latest
33 | needs: release
34 |
35 | steps:
36 | - name: ⬇️ Checkout project
37 | uses: actions/checkout@v3
38 |
39 | - name: 💻 Log in to Docker Hub
40 | uses: docker/login-action@v2
41 | with:
42 | username: ${{ secrets.DOCKER_USERNAME }}
43 | password: ${{ secrets.DOCKER_PASSWORD }}
44 |
45 | - name: 🏷️ Extract metadata (tags, labels) for Docker
46 | id: meta
47 | uses: docker/metadata-action@v4
48 | with:
49 | images: borjapazr/angular-skeleton
50 |
51 | - name: 💽 Build and push Docker image
52 | uses: docker/build-push-action@v3
53 | with:
54 | context: .
55 | file: docker/Dockerfile
56 | push: true
57 | tags: ${{ steps.meta.outputs.tags }}
58 | labels: ${{ steps.meta.outputs.labels }}
59 |
60 | redeploy:
61 | name: 🔄 Redeploy webhook call
62 | runs-on: ubuntu-latest
63 | needs: distribute
64 |
65 | steps:
66 | - name: 🚀 Deploy Angular Skeleton webhook
67 | uses: joelwmale/webhook-action@master
68 | env:
69 | WEBHOOK_URL: ${{ secrets.REDEPLOY_WEBHOOK_URL }}
70 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | validate:
11 | name: ✅ Validate project
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [18.x]
16 |
17 | steps:
18 | - name: ⬇️ Checkout project
19 | uses: actions/checkout@v3
20 |
21 | - name: 🟢 Setup NodeJS ${{ matrix.node-version }}
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | cache: npm
26 |
27 | - name: 📥 Install dependencies
28 | run: npm ci
29 |
30 | - name: 🖍️ Check types
31 | run: npm run check:types
32 |
33 | - name: 💅 Check format
34 | run: npm run check:format
35 |
36 | - name: 📑 Check lint
37 | run: npm run check:lint
38 |
39 | - name: 🌐 Check html
40 | run: npm run check:html
41 |
42 | - name: 💄 Check scss
43 | run: npm run check:scss
44 |
45 | - name: 🔡 Check i18n
46 | run: npm run check:i18n
47 |
48 | - name: 🔤 Check spelling
49 | run: npm run check:spelling
50 |
51 | test:
52 | name: 🧑🔬 Test project
53 | runs-on: ubuntu-latest
54 | needs: validate
55 | strategy:
56 | matrix:
57 | node-version: [18.x]
58 |
59 | steps:
60 | - name: ⬇️ Checkout project
61 | uses: actions/checkout@v3
62 |
63 | - name: 🟢 Setup NodeJS ${{ matrix.node-version }}
64 | uses: actions/setup-node@v3
65 | with:
66 | node-version: ${{ matrix.node-version }}
67 | cache: npm
68 |
69 | - name: 📥 Install dependencies
70 | run: npm ci
71 |
72 | - name: 🧪 Run the tests
73 | run: npm run test:coverage
74 |
75 | build:
76 | name: 🧰 Build project
77 | runs-on: ubuntu-latest
78 | needs: test
79 | strategy:
80 | matrix:
81 | node-version: [18.x]
82 |
83 | steps:
84 | - name: ⬇️ Checkout project
85 | uses: actions/checkout@v3
86 |
87 | - name: 🟢 Setup NodeJS ${{ matrix.node-version }}
88 | uses: actions/setup-node@v3
89 | with:
90 | node-version: ${{ matrix.node-version }}
91 | cache: npm
92 |
93 | - name: 📥 Install dependencies
94 | run: npm ci
95 |
96 | - name: ⚒️ Build project in SSR mode
97 | run: npm run build:ssr:prod
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled output
2 | /dist
3 | /tmp
4 | /out-tsc
5 | /bazel-out
6 |
7 | # Node
8 | /node_modules
9 | npm-debug.log
10 | yarn-error.log
11 |
12 | # IDEs and editors
13 | .idea/
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # Visual Studio Code
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 | .history/*
28 |
29 | # Miscellaneous
30 | /.angular/cache
31 | .sass-cache/
32 | /connect.lock
33 | /coverage*
34 | /libpeerconnection.log
35 | testem.log
36 | /typings
37 |
38 | # System files
39 | .DS_Store
40 | Thumbs.db
41 |
42 | # Dependencies
43 | node_modules
44 | .pnp
45 | .pnp.js
46 | .npm
47 |
48 | # Testing
49 | coverage*
50 | cypress/videos
51 | cypress/screenshots
52 | .nyc_output
53 |
54 | # Production
55 | dist
56 |
57 | # Misc
58 | .DS_Store
59 | .AppleDouble
60 | .LSOverride
61 |
62 | # Logging
63 | npm-debug.log*
64 | yarn-debug.log*
65 | yarn-error.log*
66 | *.log
67 | logs
68 |
69 | # IDE
70 | .idea
71 |
72 | # Runtime data
73 | pids
74 | *.pid
75 | *.seed
76 |
77 | # Lint
78 | .eslintcache
79 | .stylelintcache
80 |
81 | # App data
82 | .docker
83 |
84 |
--------------------------------------------------------------------------------
/.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": false,
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": false,
15 | "style-disabled": true,
16 | "inline-style-disabled": false,
17 | "inline-script-disabled": true,
18 | "space-tab-mixed-disabled": true,
19 | "id-class-ad-disabled": true,
20 | "attr-unsafe-chars": true
21 | }
22 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | # The reason we're exporting this variable is because of this issue:
5 | # https://github.com/typicode/husky/issues/968
6 | export FORCE_COLOR=1
7 |
8 | npx commitlint --edit $1 ||
9 | (
10 | echo '✍📤 It seems that the format of the commit does not follow the conventional commit convention. You can also try committing with the "npm run commit" command.';
11 | false;
12 | )
13 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | # The reason we're exporting this variable is because of this issue:
5 | # https://github.com/typicode/husky/issues/968
6 | export FORCE_COLOR=1
7 |
8 | echo '🔍🎨 Formating and checking staged files before committing!'
9 |
10 | npx lint-staged ||
11 | (
12 | echo '💀❌ Ooops! Formating and checking process has failed!';
13 | false;
14 | )
15 |
16 | echo '🥳✅ Formating and checking process has been successfully completed!'
17 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '*.{js,ts}': [
3 | 'prettier --check --write --ignore-unknown',
4 | 'eslint --cache --color --fix',
5 | () => 'tsc --pretty --noEmit'
6 | ],
7 | '!*.{js,ts}': ['prettier --check --write --ignore-unknown'],
8 | '*.html': ['htmlhint'],
9 | '*.scss': ['stylelint --cache --color --fix'],
10 | 'src/assets/i18n/**/*.json': ['transloco-validator'],
11 | '{README.md,TODO.md,.github/*.md,src/**/*.ts}': ['cspell']
12 | };
13 |
--------------------------------------------------------------------------------
/.ncurc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | upgrade: true,
3 | reject: ['@types/node']
4 | };
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .commitlintrc*
2 | .docker
3 | .DS_Store
4 | .editorconfig
5 | .eslintcache
6 | .stylelintcache
7 | .eslintignore
8 | .eslintrc*
9 | .nycrc
10 | .prettierrc*
11 | .travis.yml
12 | *.log
13 | coverage
14 | logs
15 | node_modules
16 | README.md
17 | test
18 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Directories
2 | .yarn/
3 | .docker/
4 | **/.coverage_artifacts
5 | **/.coverage_cache
6 | **/.coverage_contracts
7 | **/artifacts
8 | **/build
9 | **/cache
10 | **/coverage
11 | **/dist
12 | **/logs
13 | **/node_modules
14 | **/types
15 |
16 | # Files
17 | .commitlintrc*
18 | .eslintcache
19 | .stylelintcache
20 | .pnp.*
21 | *.env
22 | *.log
23 | coverage.json
24 | npm-debug.log*
25 | package.json
26 | package-lock.json
27 | yarn.lock
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | endOfLine: 'lf',
3 | arrowParens: 'avoid',
4 | printWidth: 120,
5 | quoteProps: 'as-needed',
6 | semi: true,
7 | singleQuote: true,
8 | tabWidth: 2,
9 | trailingComma: 'none',
10 | overrides: [
11 | {
12 | files: '*.ts',
13 | options: { parser: 'typescript' }
14 | }
15 | ]
16 | };
17 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | *.*
4 | !*.css
5 | !*.scss
6 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 18.13.0
2 |
--------------------------------------------------------------------------------
/.versionrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | header: '# Changelog\n\nAll notable changes to this project will be documented in this file.\n',
3 | types: [
4 | { type: 'chore', section: 'Others', hidden: false },
5 | { type: 'revert', section: 'Reverts', hidden: false },
6 | { type: 'feat', section: 'Features', hidden: false },
7 | { type: 'fix', section: 'Bug Fixes', hidden: false },
8 | { type: 'improvement', section: 'Feature Improvements', hidden: false },
9 | { type: 'docs', section: 'Docs', hidden: false },
10 | { type: 'style', section: 'Styling', hidden: false },
11 | { type: 'refactor', section: 'Code Refactoring', hidden: false },
12 | { type: 'perf', section: 'Performance Improvements', hidden: false },
13 | { type: 'test', section: 'Tests', hidden: false },
14 | { type: 'build', section: 'Build System', hidden: false },
15 | { type: 'ci', section: 'CI', hidden: false }
16 | ],
17 | scripts: {
18 | postchangelog: 'prettier -w CHANGELOG.md'
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "angular.ng-template",
4 | "dbaeumer.vscode-eslint",
5 | "eamodio.gitlens",
6 | "esbenp.prettier-vscode",
7 | "mkaufman.HTMLHint",
8 | "streetsidesoftware.code-spell-checker",
9 | "stylelint.vscode-stylelint",
10 | "vsls-contrib.codetour",
11 | "bradlc.vscode-tailwindcss",
12 | "firsttris.vscode-jest-runner",
13 | "Orta.vscode-jest"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.enabled": true,
3 | "cSpell.userWords": [], // only use words from .cspell.json
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true,
6 | "source.fixAll.format": true,
7 | "source.fixAll.stylelint": true,
8 | "source.fixAll.htmlhint": true
9 | },
10 | "editor.defaultFormatter": "esbenp.prettier-vscode",
11 | "editor.formatOnPaste": true,
12 | "editor.formatOnSave": true,
13 | "eslint.alwaysShowStatus": true,
14 | "eslint.options": {
15 | "extensions": [".js", ".ts", ".html"]
16 | },
17 | "eslint.validate": ["javascript", "typescript", "html"],
18 | "htmlhint.enable": true,
19 | "css.validate": false,
20 | "less.validate": false,
21 | "scss.validate": false,
22 | "errorLens.gutterIconsEnabled": true,
23 | "errorLens.gutterIconSet": "borderless",
24 | "errorLens.followCursor": "activeLine",
25 | "stylelint.enable": true,
26 | "stylelint.validate": ["css", "scss"]
27 | }
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [1.3.0](https://github.com/borjapazr/angular-skeleton/compare/v1.2.6...v1.3.0) (2023-03-07)
6 |
7 | ### Features
8 |
9 | - **deps:** upgrade angular to v15 ([2b354ef](https://github.com/borjapazr/angular-skeleton/commit/2b354ef99e04ff7dd7199040aa06fdcf80702723)), closes [#100](https://github.com/borjapazr/angular-skeleton/issues/100)
10 |
11 | ### Others
12 |
13 | - **deps:** update dependencies ([315c6a1](https://github.com/borjapazr/angular-skeleton/commit/315c6a104419115aa2dd72cc2ec14df07f47b727))
14 |
15 | ### [1.2.6](https://github.com/borjapazr/angular-skeleton/compare/v1.2.5...v1.2.6) (2023-01-20)
16 |
17 | ### Others
18 |
19 | - **actions:** update setup-node action version to v3 ([c0b3b7e](https://github.com/borjapazr/angular-skeleton/commit/c0b3b7eaedcc61bbda5fec4790edc6b2ad056937))
20 |
21 | ### [1.2.5](https://github.com/borjapazr/angular-skeleton/compare/v1.2.4...v1.2.5) (2023-01-20)
22 |
23 | ### Others
24 |
25 | - **deps:** update dependencies ([aef02ff](https://github.com/borjapazr/angular-skeleton/commit/aef02ff6dd9b953041acce01a558dfeafbc7b3f1))
26 | - **git:** update .gitignore ([c056e5c](https://github.com/borjapazr/angular-skeleton/commit/c056e5c0790c7921f4db62845eee6818c8553657))
27 |
28 | ### [1.2.4](https://github.com/borjapazr/angular-skeleton/compare/v1.2.3...v1.2.4) (2023-01-10)
29 |
30 | ### Bug Fixes
31 |
32 | - **version:** add postchangelog script ([45da200](https://github.com/borjapazr/angular-skeleton/commit/45da2004423cbfe6bec247bee67371f5e427bf68))
33 |
34 | ### Others
35 |
36 | - **deps:** update and add peer dependencies explicitly ([d7077c9](https://github.com/borjapazr/angular-skeleton/commit/d7077c9b7da007fe7fbaf65c636790a7536ed1c2))
37 |
38 | ### [1.2.3](https://github.com/borjapazr/angular-skeleton/compare/v1.2.2...v1.2.3) (2022-12-18)
39 |
40 | ### Bug Fixes
41 |
42 | - **badge:** fix shieldsio badge ([709c7b2](https://github.com/borjapazr/angular-skeleton/commit/709c7b23c155e7214820328cf007327879f92f53))
43 |
44 | ### Others
45 |
46 | - **deps:** update dependencies ([eed89c1](https://github.com/borjapazr/angular-skeleton/commit/eed89c1be4224fdb9ba7be34220149ce8ba40e48))
47 | - **deps:** update dependencies ([9cb9b31](https://github.com/borjapazr/angular-skeleton/commit/9cb9b3189576a608fbf92322a37df2da97827b46))
48 |
49 | ### [1.2.2](https://github.com/borjapazr/angular-skeleton/compare/v1.2.1...v1.2.2) (2022-10-26)
50 |
51 | ### Others
52 |
53 | - **deps:** update dependencies ([71b987f](https://github.com/borjapazr/angular-skeleton/commit/71b987f46d21dbeaa0e6ae5c4730e99cc4a78b26))
54 | - **deps:** update dependencies ([435f0c3](https://github.com/borjapazr/angular-skeleton/commit/435f0c33b7d8852e53475be16452f7e473b037e6))
55 | - **deps:** update dependencies ([4e965b5](https://github.com/borjapazr/angular-skeleton/commit/4e965b58e1bcdd10c08990ceb94692d757719dd7))
56 |
57 | ### [1.2.1](https://github.com/borjapazr/angular-skeleton/compare/v1.2.0...v1.2.1) (2022-10-04)
58 |
59 | ### Others
60 |
61 | - **deps:** update dependencies ([3f52014](https://github.com/borjapazr/angular-skeleton/commit/3f52014f449e27a889df852b2d4f6d8b1ac6b367))
62 | - **deps:** update dependencies ([d53f914](https://github.com/borjapazr/angular-skeleton/commit/d53f9148081f5b5910bea7fa85ce336b05b1fc84))
63 | - **deps:** update dependencies ([149ea43](https://github.com/borjapazr/angular-skeleton/commit/149ea4338dcd1a6085d902cecd8de10a23301cfb))
64 |
65 | ## [1.2.0](https://github.com/borjapazr/angular-skeleton/compare/v1.1.5...v1.2.0) (2022-08-10)
66 |
67 | ### Features
68 |
69 | - **version:** show app version in header ([260c500](https://github.com/borjapazr/angular-skeleton/commit/260c500a72192f5fac2d3e192dc127f828bb36e6))
70 |
71 | ### [1.1.5](https://github.com/borjapazr/angular-skeleton/compare/v1.1.4...v1.1.5) (2022-08-06)
72 |
73 | ### Bug Fixes
74 |
75 | - **transferstate:** fix deprecation warnings about transferstate (browser and server) ([2b3ef0c](https://github.com/borjapazr/angular-skeleton/commit/2b3ef0cc2ef53c446fd3d9a23b4f5d33aa0dba1a))
76 |
77 | ### [1.1.4](https://github.com/borjapazr/angular-skeleton/compare/v1.1.3...v1.1.4) (2022-08-06)
78 |
79 | ### Others
80 |
81 | - **deps:** update angular dependencies ([26d0dac](https://github.com/borjapazr/angular-skeleton/commit/26d0dac77250d52af109e2756e43237aa1b49535))
82 | - **deps:** update dependencies ([b221ca8](https://github.com/borjapazr/angular-skeleton/commit/b221ca82b63084940be49e3ee17a23bdbb1d514c))
83 | - **deps:** update dependencies ([d1d1eaa](https://github.com/borjapazr/angular-skeleton/commit/d1d1eaa327e15ac4ab3a5fd5e18684c730d2ab38))
84 | - **deps:** update dependencies ([f3198ad](https://github.com/borjapazr/angular-skeleton/commit/f3198ad000cf22d980d2042ef1053ba8166c1339))
85 | - **eslint:** format tests section ([79b60d0](https://github.com/borjapazr/angular-skeleton/commit/79b60d074355951d1584afc552200eddd6eb771d))
86 |
87 | ### [1.1.3](https://github.com/borjapazr/angular-skeleton/compare/v1.1.2...v1.1.3) (2022-07-26)
88 |
89 | ### Others
90 |
91 | - **ci:** add node 18.x validations ([5110965](https://github.com/borjapazr/angular-skeleton/commit/5110965f0a26a226794c5ed53a173237769e839a))
92 |
93 | ### [1.1.2](https://github.com/borjapazr/angular-skeleton/compare/v1.1.1...v1.1.2) (2022-07-26)
94 |
95 | ### Others
96 |
97 | - **angular:** add allowed commonjs libs to angular config ([2be8210](https://github.com/borjapazr/angular-skeleton/commit/2be8210f4ed249a9e03b9a2b439ce7890830e00c))
98 | - **deps:** update dependencies ([ea65e69](https://github.com/borjapazr/angular-skeleton/commit/ea65e697a86a9b94fdcb7e08bd99b2857353cae5))
99 | - **deps:** update dependencies ([dc44cc5](https://github.com/borjapazr/angular-skeleton/commit/dc44cc59178b30c6ec50e1ec60c44444852f36bf))
100 |
101 | ### [1.1.1](https://github.com/borjapazr/angular-skeleton/compare/v1.1.0...v1.1.1) (2022-07-11)
102 |
103 | ### Others
104 |
105 | - **deps:** update dependencies ([546193f](https://github.com/borjapazr/angular-skeleton/commit/546193f3a9afbb6b7e658bc8042dc84816307351))
106 |
107 | ## [1.1.0](https://github.com/borjapazr/angular-skeleton/compare/v1.0.3...v1.1.0) (2022-06-30)
108 |
109 | ### Features
110 |
111 | - **footer:** add github link ([6fdd968](https://github.com/borjapazr/angular-skeleton/commit/6fdd968d65917628eb40ac870cd0638e34ef9c8c))
112 |
113 | ### Docs
114 |
115 | - **readme:** add wip message in several sections ([797e8fa](https://github.com/borjapazr/angular-skeleton/commit/797e8faedf9266c7c0dafe00f2f095df738cc45c))
116 |
117 | ### Others
118 |
119 | - **ci:** migrate to get-tsconfig ([9f903bb](https://github.com/borjapazr/angular-skeleton/commit/9f903bb2978f45a0085e1a9faca5e84849faf2ff))
120 |
121 | ### [1.0.3](https://github.com/borjapazr/angular-skeleton/compare/v1.0.2...v1.0.3) (2022-06-26)
122 |
123 | ### Others
124 |
125 | - **deps:** update dependencies ([dd18d5b](https://github.com/borjapazr/angular-skeleton/commit/dd18d5b93e9fcc493032221a0756c84b13e76bed))
126 |
127 | ### Docs
128 |
129 | - **readme:** add demo badge ([8b1b14c](https://github.com/borjapazr/angular-skeleton/commit/8b1b14cd59bfa73a6b6a2828b90677891dc3c457))
130 | - **readme:** add documentation status badge ([e9b53d1](https://github.com/borjapazr/angular-skeleton/commit/e9b53d1556194e88c565299711545387eb2c5840))
131 |
132 | ### [1.0.2](https://github.com/borjapazr/angular-skeleton/compare/v1.0.1...v1.0.2) (2022-06-22)
133 |
134 | ### Bug Fixes
135 |
136 | - **i18n:** change return value of localize route guard ([b5e3c31](https://github.com/borjapazr/angular-skeleton/commit/b5e3c310df5d13fd1802400d36501727240aa3fe))
137 |
138 | ### Docs
139 |
140 | - **readme:** add content to features section ([5f040ce](https://github.com/borjapazr/angular-skeleton/commit/5f040ce0f6ff4e8bf158ffe77b420d21a99a6b14))
141 |
142 | ### [1.0.1](https://github.com/borjapazr/angular-skeleton/compare/v1.0.0...v1.0.1) (2022-06-22)
143 |
144 | ### Bug Fixes
145 |
146 | - **router:** fix i18n guard in order to replace state ([bf9fe42](https://github.com/borjapazr/angular-skeleton/commit/bf9fe42eb697f7bae237485e4198ba8b9087eba6))
147 |
148 | ### Docs
149 |
150 | - **readme:** add a README.md template ([7f82c10](https://github.com/borjapazr/angular-skeleton/commit/7f82c10c5c6fe507fc0db2139683c604a5381392))
151 | - **readme:** add lighthouse report image ([84e2e94](https://github.com/borjapazr/angular-skeleton/commit/84e2e94cfabbcf98980ef6e1f3e34d4dd29dea2b))
152 | - **readme:** update lighthouse image ([1dd2775](https://github.com/borjapazr/angular-skeleton/commit/1dd2775b81ab8df8108f70e79b67604605ec58b2))
153 |
154 | ## [1.0.0](https://github.com/borjapazr/angular-skeleton/compare/v0.0.2...v1.0.0) (2022-06-21)
155 |
156 | ### Bug Fixes
157 |
158 | - **footer:** remove author on small devices ([a82f374](https://github.com/borjapazr/angular-skeleton/commit/a82f37450889895abb475160e1017184275d942c))
159 |
160 | ### [0.0.2](https://github.com/borjapazr/angular-skeleton/compare/v0.0.1...v0.0.2) (2022-06-21)
161 |
162 | ### Features
163 |
164 | - **cd:** add deploy step ([ad44e9d](https://github.com/borjapazr/angular-skeleton/commit/ad44e9df310479521f3576015534c0a81fdfcbad))
165 |
166 | ### [0.0.1](https://github.com/borjapazr/angular-skeleton/compare/v0.0.0...v0.0.1) (2022-06-21)
167 |
168 | ### Bug Fixes
169 |
170 | - **cd:** add permissions to cd workflow ([77b5376](https://github.com/borjapazr/angular-skeleton/commit/77b5376ab366b27f769296746baea0b9115e7696))
171 |
172 | ## 0.0.0 (2022-06-21)
173 |
174 | ### Features
175 |
176 | - **cd:** add job to publish docker image ([9e16cff](https://github.com/borjapazr/angular-skeleton/commit/9e16cffb0dbc7cb2d1f2409f0b0ac29fc7b06974))
177 |
178 | ### Others
179 |
180 | - **init:** let's start this party ([dcf4544](https://github.com/borjapazr/angular-skeleton/commit/dcf4544c39ceb9e9eaa9a08c10336dadc0a83daa))
181 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Borja Paz Rodríguez borjapazr@gmail.com
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ## Include .env file
2 | include .env
3 |
4 | ## Root directory
5 | ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
6 |
7 | ## Set 'bash' as default shell
8 | SHELL := $(shell which bash)
9 |
10 | ## Set 'help' target as the default goal
11 | .DEFAULT_GOAL := help
12 |
13 | .PHONY: help
14 | help: ## Show this help
15 | @egrep -h '^[a-zA-Z0-9_\/-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort -d | awk 'BEGIN {FS = ":.*?## "; printf "Usage: make \033[0;34mTARGET\033[0m \033[0;35m[ARGUMENTS]\033[0m\n\n"; printf "Targets:\n"}; {printf " \033[33m%-25s\033[0m \033[0;32m%s\033[0m\n", $$1, $$2}'
16 |
17 | ## Target specific variables
18 | %/csr: MODE = csr
19 | %/ssr: MODE = ssr
20 | build/%: TAG ?= $(MODE)
21 |
22 | .PHONY: build/csr build/ssr
23 | build/csr: ## Build csr image
24 | build/ssr: ## Build ssr image
25 | build/csr build/ssr:
26 | @echo "📦 Building project Docker image..."
27 | @docker build --build-arg PORT=$(PORT) --target $(MODE) -t $(APP_NAME):$(TAG) -f ./docker/Dockerfile .
28 |
29 | .PHONY: start/csr start/ssr
30 | start/csr: ## Start application in Client Side Rendering mode
31 | start/ssr: ## Start application in Server Side Rendering mode
32 | start/csr start/ssr:
33 | @echo "▶️ Starting app in $(MODE) mode (Docker)..."
34 | @docker-compose -f ./docker/docker-compose.$(MODE).yml --env-file .env up --build
35 |
36 | .PHONY: stop/csr stop/ssr
37 | stop/csr: ## Stop application in Client Side Rendering mode
38 | stop/ssr: ## Stop application in Server Side Rendering mode
39 | stop/csr stop/ssr:
40 | @echo "🛑 Stopping app..."
41 | @docker-compose -f ./docker/docker-compose.$(MODE).yml --env-file .env down
42 |
43 | .PHONY: clean/csr clean/ssr
44 | clean/csr: ## Clean CSR application
45 | clean/ssr: ## Clean SSR application
46 | clean/csr clean/ssr:
47 | @echo "🧼 Cleaning all resources..."
48 | @docker-compose -f ./docker/docker-compose.$(MODE).yml --env-file .env down --rmi local --volumes --remove-orphans
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
13 |
14 | 
15 | 
16 | 
17 | 
18 | [](https://angular-skeleton.marsmachine.space/)
19 | [](https://img.shields.io/badge/documentation-80%25-orange?style=flat-square)
20 |
21 |
22 | 🅰️🦸 Production-ready template for Progressive Web Applications implemented with Angular, TailwindCSS, Transloco, ngx-isr, etc.
23 |
24 |
25 |
ℹ️ About •
26 |
📋 Features •
27 |
🤝 Contributing •
28 |
🛣️ Roadmap •
29 |
🎯 Credits •
30 |
🚩 License
31 |
32 |
33 |
34 | ---
35 |
36 | ## ℹ️ About
37 |
38 | The main goal of this project is to provide a base template for the generation of a production-ready web application made with `Angular`. The idea is to avoid having to configure all the tools involved in a project every time it is started and thus be able to focus on the definition and implementation of the business logic.
39 |
40 | > 📣 This is an opinionated template. The architecture of the code base and the configuration of the different tools used has been based on best practices and personal preferences.
41 |
42 | ### 🚀 Quick start
43 |
44 | - Start project in development mode:
45 |
46 | ```bash
47 | npm run start:dev
48 | ```
49 |
50 | - Start project in production mode:
51 |
52 | ```bash
53 | npm run start:prod
54 | ```
55 |
56 | ## 📋 Features
57 |
58 | - [Angular](https://angular.io/): Angular is a platform for building mobile and desktop web applications.
59 | - [Angular Universal](https://angular.io/guide/universal): Server-side rendering (SSR) with Angular Universal.
60 | - [TailwindCSS](https://tailwindcss.com/): A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.
61 | - [ng-lazyload-image](https://www.npmjs.com/package/ng-lazyload-image): A super small libary for lazy loading images for Angular apps with zero dependencies
62 | - [ngx-isr](https://www.npmjs.com/package/ngx-isr): Incremental Static Regeneration (ISR) enables developers and content editors to use static-generation on a per-page basis, without needing to rebuild the entire site. With ISR, you can retain the benefits of static while scaling to millions of pages.
63 | - i18n using [Transloco](https://ngneat.github.io/transloco/)
64 | - Unit tests using [Jest](https://github.com/facebook/jest)
65 | - e2e tests using [Cypress](https://www.cypress.io/)
66 | - [Spell check](https://github.com/streetsidesoftware/cspell)
67 | - Linting with [ESLint](https://github.com/eslint/eslint)
68 | - Formatting with [Prettier](https://github.com/prettier/prettier)
69 | - [Stylelint](https://stylelint.io/): A mighty, modern linter that helps you avoid errors and enforce conventions in your styles.
70 | - [HTMLHint](https://htmlhint.com/): A linter for HTML that helps you avoid errors and enforce conventions in your HTML.
71 | - Commit messages must meet conventional commits format
72 | - Git hooks with [Husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged)
73 | - Containerised using [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/)
74 | - GitHub Actions
75 | - Makefile as project entrypoint
76 | - A lot of emojis 🛸
77 |
78 | ### 🗂 Codebase structure
79 |
80 | ```txt
81 | angular-skeleton/
82 | ├── .github/
83 | ├── .husky/
84 | ├── .vscode/
85 | ├── cypress/
86 | │ ├── e2e/
87 | │ ├── fixtures/
88 | │ ├── support/
89 | │ ├── coverage.webpack.js
90 | │ └── tsconfig.json
91 | ├── docker/
92 | ├── src/
93 | │ ├── app/
94 | │ │ ├── core/
95 | │ │ │ ├── components/
96 | │ │ │ ├── constants/
97 | │ │ │ ├── enums/
98 | │ │ │ ├── guards/
99 | │ │ │ ├── handlers/
100 | │ │ │ ├── interceptors/
101 | │ │ │ ├── loaders/
102 | │ │ │ ├── models/
103 | │ │ │ ├── resolvers/
104 | │ │ │ ├── services/
105 | │ │ │ ├── strategies/
106 | │ │ │ ├── tokens/
107 | │ │ │ ├── utils/
108 | │ │ │ ├── ...
109 | │ │ │ └── core.module.ts
110 | │ │ ├── features
111 | │ │ │ ├── feature-a
112 | │ │ │ │ ├── components/
113 | │ │ │ │ ├── models/
114 | │ │ │ │ ├── pages/
115 | │ │ │ │ ├── services/
116 | │ │ │ │ ├── ...
117 | │ │ │ │ ├── home-routing.module.ts
118 | │ │ │ │ └── home.module.ts
119 | │ │ │ ├── feature-b
120 | │ │ │ └── ...
121 | │ │ ├── shared/
122 | │ │ │ ├── components/
123 | │ │ │ ├── directives/
124 | │ │ │ ├── modules/
125 | │ │ │ ├── pipes/
126 | │ │ │ ├── services/
127 | │ │ │ └── shared.module.ts
128 | │ │ ├── app-routing.module.ts
129 | │ │ ├── app.browser.module.ts
130 | │ │ ├── app.component.html
131 | │ │ ├── app.component.scss
132 | │ │ ├── app.component.ts
133 | │ │ ├── app.module.ts
134 | │ │ └── app.server.module.ts
135 | │ ├── assets/
136 | │ │ ├── i18n/
137 | │ │ ├── icons/
138 | │ │ └── images/
139 | │ ├── environments/
140 | │ ├── styles/
141 | │ │ ├── abstracts/
142 | │ │ ├── base/
143 | │ │ ├── components/
144 | │ │ ├── layout/
145 | │ │ ├── vendors/
146 | │ │ └── main.scss
147 | │ ├── types/
148 | │ ├── favicon.ico
149 | │ ├── favicon.png
150 | │ ├── index.html
151 | │ ├── jest.mocks.ts
152 | │ ├── jest.setup.ts
153 | │ ├── main.browser.ts
154 | │ ├── main.server.ts
155 | │ ├── manifest.webmanifest
156 | │ ├── polyfills.ts
157 | │ ├── robots.txt
158 | │ └── styles.scss
159 | ├── .browserslistrc
160 | ├── .commitlintrc.js
161 | ├── .cspell.json
162 | ├── .czrc
163 | ├── .dockerignore
164 | ├── .editorconfig
165 | ├── .env
166 | ├── .eslintcache
167 | ├── .eslintignore
168 | ├── .eslintrc.js
169 | ├── .gitignore
170 | ├── .htmlhintrc
171 | ├── .lintstagedrc.js
172 | ├── .ncurc.js
173 | ├── .npmignore
174 | ├── .prettierignore
175 | ├── .prettierrc.js
176 | ├── .stylelintcache
177 | ├── .stylelintignore
178 | ├── .tool-versions
179 | ├── .versionrc.js
180 | ├── CHANGELOG.md
181 | ├── LICENSE
182 | ├── Makefile
183 | ├── README.md
184 | ├── TODO.md
185 | ├── angular.json
186 | ├── cypress.config.ts
187 | ├── jest.config.js
188 | ├── ngsw-config.json
189 | ├── nyc.config.js
190 | ├── package-lock.json
191 | ├── package.json
192 | ├── routes.txt
193 | ├── server.ts
194 | ├── stylelint.config.js
195 | ├── tailwind.config.js
196 | ├── transloco.config.js
197 | ├── tsconfig.app.json
198 | ├── tsconfig.json
199 | ├── tsconfig.server.json
200 | └── tsconfig.spec.json
201 | ```
202 |
203 | ### 🎛️ Code style and best practices
204 |
205 | > ⚠️ This section has yet to be fully documented.
206 |
207 | - Prettier
208 | - ESLint
209 | - Stylelint
210 | - HTMLHint
211 | - commitlint
212 |
213 | ### 🛢 Barrel files
214 |
215 | Barrel files are used to organize exports. This significantly reduces the size of the import blocks.
216 |
217 | ### 🏞 Application layout
218 |
219 | > ⚠️ This section has yet to be fully documented.
220 |
221 | - Flexbox layout
222 | - TailwindCSS
223 | - Dark theme
224 | - Styles (SCSS) folder structure
225 |
226 | ### 🌐 Internationalization (i18n)
227 |
228 | > ⚠️ This section has yet to be fully documented.
229 |
230 | - Transloco
231 | - Route language prefixing
232 |
233 | ### 🏎 Server Side Rendering (SSR) and Incremental Static Rendering (ISR)
234 |
235 | > ⚠️ This section has yet to be fully documented.
236 |
237 | - Angular Universal
238 | - Domino
239 | - ngx-isr
240 |
241 | ### 📇 Prerendering
242 |
243 | > ⚠️ This section has yet to be fully documented.
244 |
245 | - Angular Universal
246 |
247 | ### 📈 SEO
248 |
249 | > ⚠️ This section has yet to be fully documented.
250 |
251 | - CustomPageTitleStrategy
252 |
253 | ### 🔰 Progressive Web Application (PWA)
254 |
255 | > ⚠️ This section has yet to be fully documented.
256 |
257 | - Service Worker configuration
258 | - Stale while revalidate strategy
259 | - Offline support
260 |
261 | ### 🌠 Image lazy-loading
262 |
263 | > ⚠️ This section has yet to be fully documented.
264 |
265 | - ng-lazyload-image
266 |
267 | ### 💨 Module preloading strategies
268 |
269 | > ⚠️ This section has yet to be fully documented.
270 |
271 | - [NoPreloading](https://angular.io/api/router/NoPreloading) (default)
272 | - [PreloadAllModules](https://angular.io/api/router/PreloadAllModules)
273 | - [CustomRoutePreloadStrategy](src/app/core/strategies/custom-route-preload.strategy.ts)
274 | - [NetworkAwareRoutePreloadingStrategy](src/app/core/strategies/network-aware-route-preload.strategy.ts)
275 | - [HoverPreloadStrategy](https://github.com/mgechev/ngx-hover-preload/blob/master/projects/ngx-hover-preload/src/lib/hover-preload.strategy.ts)
276 | - [QuicklinkStrategy](https://github.com/mgechev/ngx-quicklink/blob/master/src/quicklink-strategy.service.ts)
277 |
278 | ### 🛣 Route reusability
279 |
280 | - RouteReuseStrategy
281 |
282 | ### 🏒 Pipes
283 |
284 | > ⚠️ This section has yet to be fully documented.
285 |
286 | ### 🧪 Testing
287 |
288 | > ⚠️ This section has yet to be fully documented.
289 |
290 | #### Unit and integration tests
291 |
292 | - Jest
293 | - jest-extended
294 |
295 | #### e2e tests
296 |
297 | - Cypress
298 |
299 | ### 🐐 Makefile rules
300 |
301 | The main actions on this project are managed using a [Makefile](Makefile) as an entrypoint.
302 |
303 | ```bash
304 | Usage: make TARGET [ARGUMENTS]
305 |
306 | Targets:
307 | build/csr Build csr image
308 | build/ssr Build ssr image
309 | clean/csr Clean CSR application
310 | clean/ssr Clean SSR application
311 | help Show this help
312 | start/csr Start application in Client Side Rendering mode
313 | start/ssr Start application in Server Side Rendering mode
314 | stop/csr Stop application in Client Side Rendering mode
315 | stop/ssr Stop application in Server Side Rendering mode
316 | ```
317 |
318 | ### ⚡ Scripts
319 |
320 | [package.json](package.json) scripts:
321 |
322 | ```json
323 | ...
324 | "scripts": {
325 | "start:dev": "ng serve --configuration development --port 4200 --open",
326 | "start:prod": "ng serve --configuration production --port 4300 --open",
327 | "start:ssr:dev": "ng run angular-skeleton:serve-ssr:development --port 4201 --open",
328 | "start:ssr:prod": "ng run angular-skeleton:serve-ssr:production --port 4301 --open",
329 | "build:dev": "rimraf dist && ng build --configuration development",
330 | "build:prod": "rimraf dist && ng build --configuration production && npm run build:optimize",
331 | "build:ssr:dev": "rimraf dist && ng build --configuration development && ng run angular-skeleton:server:development",
332 | "build:ssr:prod": "rimraf dist && ng build --configuration production && ng run angular-skeleton:server:production && npm run build:optimize",
333 | "build:prerender:dev": "rimraf dist && ng run angular-skeleton:prerender:development",
334 | "build:prerender:prod": "rimraf dist && ng run angular-skeleton:prerender:production && npm run build:optimize",
335 | "build:optimize": "run-s optimize:* && ngsw-config dist/browser ./ngsw-config.json",
336 | "optimize:i18n": "transloco-optimize dist/browser/assets/i18n",
337 | "serve:pwa": "http-server -p 4400 -P http://localhost:4400? dist/browser -o",
338 | "serve:ssr": "node dist/server/main.js",
339 | "i18n:extract": "transloco-keys-manager extract",
340 | "i18n:find": "transloco-keys-manager find",
341 | "check:types": "tsc --pretty --noEmit && tsc --project cypress/tsconfig.json --pretty --noEmit",
342 | "check:format": "prettier --check .",
343 | "check:lint": "eslint . --ext .js,.ts --color",
344 | "check:html": "htmlhint .",
345 | "check:scss": "stylelint 'src/**/*.{css,scss}' --color",
346 | "check:spelling": "cspell --config=.cspell.json \"{README.md,TODO.md,.github/*.md,src/**/*.ts,src/**/*.json}\"",
347 | "check:i18n": "transloco-validator src/assets/i18n/*.json src/assets/i18n/**/*.json",
348 | "check:staged": "lint-staged",
349 | "fix:format": "prettier --check --write --ignore-unknown .",
350 | "fix:lint": "npm run check:lint -- --fix",
351 | "fix:scss": "npm run check:scss -- --fix",
352 | "test": "cross-env NODE_ENV=test jest --verbose --colors --runInBand",
353 | "test:spec": "npm run test -- --testPathPattern=spec",
354 | "test:unit": "npm run test -- --testPathPattern=unit",
355 | "test:int": "npm run test -- --testPathPattern=integration",
356 | "e2e": "ng e2e",
357 | "e2e:run": "ng run angular-skeleton:cypress-run",
358 | "e2e:open": "ng run angular-skeleton:cypress-open",
359 | "e2e:coverage:view": "http-server -p 9004 ./coverage-e2e/lcov-report -o",
360 | "test:watch": "npm run test -- --watch",
361 | "test:coverage": "npm run test -- --coverage --silent",
362 | "test:coverage:view": "http-server -p 9003 ./coverage/lcov-report -o",
363 | "reset-hard": "git clean -dfx && git reset --hard && npm install",
364 | "version": "standard-version -t",
365 | "prepare-release": "run-s reset-hard version",
366 | "commit": "cz",
367 | "update-deps": "npm-check-updates -u",
368 | "prepare": "husky install"
369 | },
370 | ...
371 | ```
372 |
373 | ## 🤝 Contributing
374 |
375 | Just fork and open a pull request. All contributions are welcome 🤗
376 |
377 | ## 🛣️ Roadmap
378 |
379 | Please, check [TODO](TODO.md) for the current roadmap.
380 |
381 | ## 🎯 Credits
382 |
383 | To implement this project I have based myself on many similar projects. There were countless of them and I gave them all a star.
384 |
385 | 🙏 Thank you very much for these wonderful creations.
386 |
387 | ### ⭐ Stargazers
388 |
389 | [](https://github.com/borjapazr/angular-skeleton/stargazers)
390 |
391 | ## 🚩 License
392 |
393 | MIT @ [borjapazr](https://bpaz.dev). Please see [License](LICENSE) for more information.
394 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # 🛣️ Roadmap
2 |
3 | ### Todo
4 |
5 | - [ ] 📄 Configure the prerendering properly and fix the problem with Transloco
6 | - [ ] 📜 Document README.md
7 | - [ ] 📝 Document preloaders
8 | - [ ] 📝 Document Transloco find t(one.two.three)
9 | - [ ] 🚨 Remove deprecation warnings from RxJS
10 |
11 | ### In Progress
12 |
13 | - [ ] 🔄 Continuous improvement of the project
14 |
15 | ### Done ✓
16 |
17 | - [x] 🥇 Create the first release of the project
18 | - [x] 📤 Add Barrel exports to all components
19 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-skeleton": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | },
12 | "@schematics/angular:application": {
13 | "strict": true
14 | }
15 | },
16 | "root": "",
17 | "sourceRoot": "src",
18 | "prefix": "app",
19 | "architect": {
20 | "build": {
21 | "builder": "@angular-devkit/build-angular:browser",
22 | "options": {
23 | "allowedCommonJsDependencies": ["flat", "fast-deep-equal", "@messageformat/core"],
24 | "outputPath": "dist/browser",
25 | "index": "src/index.html",
26 | "main": "src/main.browser.ts",
27 | "polyfills": "src/polyfills.ts",
28 | "tsConfig": "tsconfig.app.json",
29 | "inlineStyleLanguage": "scss",
30 | "assets": [
31 | "src/robots.txt",
32 | "src/favicon.ico",
33 | "src/favicon.png",
34 | "src/assets",
35 | "src/manifest.webmanifest"
36 | ],
37 | "styles": ["src/styles.scss"],
38 | "scripts": [],
39 | "serviceWorker": true,
40 | "ngswConfigPath": "ngsw-config.json"
41 | },
42 | "configurations": {
43 | "production": {
44 | "budgets": [
45 | {
46 | "type": "initial",
47 | "maximumWarning": "500kb",
48 | "maximumError": "1mb"
49 | },
50 | {
51 | "type": "anyComponentStyle",
52 | "maximumWarning": "2kb",
53 | "maximumError": "4kb"
54 | }
55 | ],
56 | "fileReplacements": [
57 | {
58 | "replace": "src/environments/environment.ts",
59 | "with": "src/environments/environment.prod.ts"
60 | }
61 | ],
62 | "outputHashing": "all"
63 | },
64 | "development": {
65 | "buildOptimizer": false,
66 | "optimization": false,
67 | "vendorChunk": true,
68 | "extractLicenses": false,
69 | "sourceMap": true,
70 | "namedChunks": true
71 | }
72 | },
73 | "defaultConfiguration": "production"
74 | },
75 | "serve": {
76 | "builder": "ngx-build-plus:dev-server",
77 | "configurations": {
78 | "production": {
79 | "browserTarget": "angular-skeleton:build:production"
80 | },
81 | "development": {
82 | "browserTarget": "angular-skeleton:build:development",
83 | "extraWebpackConfig": "./cypress/coverage.webpack.js"
84 | }
85 | },
86 | "defaultConfiguration": "development"
87 | },
88 | "extract-i18n": {
89 | "builder": "@angular-devkit/build-angular:extract-i18n",
90 | "options": {
91 | "browserTarget": "angular-skeleton:build"
92 | }
93 | },
94 | "lint": {
95 | "builder": "@angular-eslint/builder:lint",
96 | "options": {
97 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
98 | }
99 | },
100 | "server": {
101 | "builder": "@angular-devkit/build-angular:server",
102 | "options": {
103 | "outputPath": "dist/server",
104 | "main": "server.ts",
105 | "tsConfig": "tsconfig.server.json",
106 | "inlineStyleLanguage": "scss"
107 | },
108 | "configurations": {
109 | "production": {
110 | "outputHashing": "media",
111 | "fileReplacements": [
112 | {
113 | "replace": "src/environments/environment.ts",
114 | "with": "src/environments/environment.prod.ts"
115 | }
116 | ]
117 | },
118 | "development": {
119 | "optimization": false,
120 | "sourceMap": true,
121 | "extractLicenses": false
122 | }
123 | },
124 | "defaultConfiguration": "production"
125 | },
126 | "serve-ssr": {
127 | "builder": "@nguniversal/builders:ssr-dev-server",
128 | "configurations": {
129 | "development": {
130 | "browserTarget": "angular-skeleton:build:development",
131 | "serverTarget": "angular-skeleton:server:development"
132 | },
133 | "production": {
134 | "browserTarget": "angular-skeleton:build:production",
135 | "serverTarget": "angular-skeleton:server:production"
136 | }
137 | },
138 | "defaultConfiguration": "development"
139 | },
140 | "prerender": {
141 | "builder": "@nguniversal/builders:prerender",
142 | "options": {
143 | "routesFile": "./routes.txt",
144 | "guessRoutes": false
145 | },
146 | "configurations": {
147 | "production": {
148 | "browserTarget": "angular-skeleton:build:production",
149 | "serverTarget": "angular-skeleton:server:production"
150 | },
151 | "development": {
152 | "browserTarget": "angular-skeleton:build:development",
153 | "serverTarget": "angular-skeleton:server:development"
154 | }
155 | },
156 | "defaultConfiguration": "production"
157 | },
158 | "test": {
159 | "builder": "@angular-builders/jest:run",
160 | "options": {
161 | "polyfills": ["src/polyfills.ts"],
162 | "tsConfig": "tsconfig.spec.json",
163 | "inlineStyleLanguage": ["scss"],
164 | "assets": [
165 | "src/robots.txt",
166 | "src/favicon.ico",
167 | "src/favicon.png",
168 | "src/assets",
169 | "src/manifest.webmanifest"
170 | ],
171 | "styles": ["src/styles.css"],
172 | "scripts": []
173 | }
174 | },
175 | "cypress-run": {
176 | "builder": "@cypress/schematic:cypress",
177 | "options": {
178 | "devServerTarget": "angular-skeleton:serve"
179 | },
180 | "configurations": {
181 | "production": {
182 | "devServerTarget": "angular-skeleton:serve:production"
183 | }
184 | }
185 | },
186 | "cypress-open": {
187 | "builder": "@cypress/schematic:cypress",
188 | "options": {
189 | "watch": true,
190 | "headless": false
191 | }
192 | },
193 | "e2e": {
194 | "builder": "@cypress/schematic:cypress",
195 | "options": {
196 | "devServerTarget": "angular-skeleton:serve",
197 | "watch": true,
198 | "headless": false
199 | },
200 | "configurations": {
201 | "production": {
202 | "devServerTarget": "angular-skeleton:serve:production"
203 | }
204 | }
205 | }
206 | }
207 | }
208 | },
209 | "cli": {
210 | "schematicCollections": ["@angular-eslint/schematics"],
211 | "analytics": false
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | defaultCommandTimeout: 10000,
5 | e2e: {
6 | baseUrl: 'http://localhost:4200',
7 | chromeWebSecurity: false,
8 | specPattern: 'cypress/e2e/**/*.{e2e,cy}.{js,ts}',
9 | setupNodeEvents: (on, config) => {
10 | require('@cypress/code-coverage/task')(on, config);
11 | return config;
12 | }
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/cypress/coverage.webpack.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | rules: [
4 | {
5 | test: /\.(js|ts)$/,
6 | loader: '@skyux-sdk/istanbul-instrumenter-loader',
7 | options: { esModules: true },
8 | enforce: 'post',
9 | include: require('path').join(__dirname, '..', 'src'),
10 | exclude: [/\.(e2e|spec|module|mock)\.ts$/, /node_modules/, /(ngfactory|ngstyle)\.js/]
11 | }
12 | ]
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/cypress/e2e/app.cy.ts:
--------------------------------------------------------------------------------
1 | describe('Welcome page', () => {
2 | it(`should render 'Welcome' message`, () => {
3 | cy.visit('/');
4 | cy.contains('Welcome!');
5 | });
6 | });
7 |
--------------------------------------------------------------------------------
/cypress/e2e/app.e2e.ts:
--------------------------------------------------------------------------------
1 | describe('Welcome page', () => {
2 | it(`should render 'Welcome' message`, () => {
3 | cy.visit('/');
4 | cy.contains('Welcome!');
5 | });
6 | });
7 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "borjapazr@gmail.com",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example namespace declaration will help
3 | // with Intellisense and code completion in your
4 | // IDE or Text Editor.
5 | // ***********************************************
6 | // declare namespace Cypress {
7 | // interface Chainable {
8 | // customCommand(param: any): typeof customCommand;
9 | // }
10 | // }
11 | //
12 | // function customCommand(param: any): void {
13 | // console.warn(param);
14 | // }
15 | //
16 | // NOTE: You can use it like so:
17 | // Cypress.Commands.add('customCommand', customCommand);
18 | //
19 | // ***********************************************
20 | // This example commands.js shows you how to
21 | // create various custom commands and overwrite
22 | // existing commands.
23 | //
24 | // For more comprehensive examples of custom
25 | // commands please read more here:
26 | // https://on.cypress.io/custom-commands
27 | // ***********************************************
28 | //
29 | //
30 | // -- This is a parent command --
31 | // Cypress.Commands.add("login", (email, password) => { ... })
32 | //
33 | //
34 | // -- This is a child command --
35 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
36 | //
37 | //
38 | // -- This is a dual command --
39 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
40 | //
41 | //
42 | // -- This will overwrite an existing command --
43 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
44 |
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/component.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // When a command from ./commands is ready to use, import with `import './commands'` syntax
17 | // import './commands';
18 |
19 | import '@cypress/code-coverage/support';
20 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": false,
4 | "types": ["cypress", "node"]
5 | },
6 | "extends": "../tsconfig.json",
7 | "files": [],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG PORT=5000
2 |
3 | # Base image stage
4 | FROM node:18-alpine as node
5 |
6 | RUN apk --no-cache -U upgrade
7 | RUN apk update
8 | RUN apk add --no-cache curl
9 | RUN npm install npm@latest -g
10 |
11 | # Builder stage
12 | FROM node AS builder
13 | ENV PATH /code/node_modules/.bin:$PATH
14 |
15 | WORKDIR /code
16 | COPY package*.json ./
17 | RUN npm install --omit=optional --ignore-scripts && npm cache clean --force
18 |
19 | WORKDIR /code/app
20 | COPY . ./
21 |
22 | # csr builder stage
23 | FROM builder as csr-builder
24 | RUN npm run build:prod
25 |
26 | # ssr builder stage
27 | FROM builder as ssr-builder
28 | RUN npm run build:ssr:prod
29 |
30 | # csr stage
31 | FROM nginx:alpine as csr
32 |
33 | LABEL maintainer "Borja Paz Rodríguez (@borjapazr)"
34 |
35 | ARG PORT
36 | ENV PORT $PORT
37 |
38 | EXPOSE $PORT
39 |
40 | RUN apk add --no-cache curl
41 |
42 | COPY --from=csr-builder /code/app/dist/browser /usr/share/nginx/html
43 | COPY ./docker/nginx/nginx.conf /etc/nginx/conf.d/default.template
44 |
45 | HEALTHCHECK --interval=30s --timeout=60s --start-period=10s --retries=3 CMD curl --fail localhost:$PORT || exit 1
46 |
47 | CMD ["/bin/sh" , "-c" , "envsubst \"`env | awk -F = '{printf \" \\\\$%s\", $1}'`\" < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
48 |
49 | # ssr stage
50 | FROM node as ssr
51 |
52 | LABEL maintainer "Borja Paz Rodríguez (@borjapazr)"
53 |
54 | ARG PORT
55 | ENV PORT $PORT
56 | ENV NODE_ENV=production
57 | ENV PATH /home/node/app/node_modules/.bin:$PATH
58 |
59 | EXPOSE $PORT
60 |
61 | WORKDIR /home/node/app
62 |
63 | RUN mkdir dist logs
64 | RUN chown -R node:node /home/node/app
65 | RUN npm install -g pm2
66 |
67 | USER node
68 |
69 | COPY --chown=node:node package*.json ./docker/pm2/process.json ./
70 | COPY --chown=node:node --from=ssr-builder /code/app/dist ./dist
71 |
72 | RUN npm ci --production --ignore-scripts --omit=optional
73 |
74 | HEALTHCHECK --interval=30s --timeout=60s --start-period=10s --retries=3 CMD curl --fail localhost:$PORT || exit 1
75 |
76 | CMD pm2-runtime ./process.json
77 |
--------------------------------------------------------------------------------
/docker/docker-compose.csr.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | angular-skeleton:
5 | build:
6 | context: ..
7 | args:
8 | - PORT=${PORT}
9 | dockerfile: ./docker/Dockerfile
10 | target: csr
11 | image: ${APP_NAME}:csr
12 | container_name: ${APP_NAME}
13 | restart: always
14 | env_file:
15 | - ../.env
16 | environment:
17 | - TZ=${TZ}
18 | ports:
19 | - ${EXTERNAL_PORT}:${PORT}
20 |
21 | networks:
22 | default:
23 | name: ${APP_NAME}-network
24 |
--------------------------------------------------------------------------------
/docker/docker-compose.ssr.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | angular-skeleton:
5 | build:
6 | context: ..
7 | args:
8 | - PORT=${PORT}
9 | dockerfile: ./docker/Dockerfile
10 | target: ssr
11 | image: ${APP_NAME}:ssr
12 | container_name: ${APP_NAME}
13 | restart: always
14 | env_file:
15 | - ../.env
16 | environment:
17 | - TZ=${TZ}
18 | ports:
19 | - ${EXTERNAL_PORT}:${PORT}
20 |
21 | networks:
22 | default:
23 | name: ${APP_NAME}-network
24 |
--------------------------------------------------------------------------------
/docker/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen ${PORT};
3 |
4 | default_type application/octet-stream;
5 | sendfile on;
6 |
7 | root /usr/share/nginx/html;
8 | index index.html index.htm;
9 |
10 | location / {
11 | try_files $uri $uri/ /index.html =404;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docker/pm2/process.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "angular-skeleton",
5 | "script": "./dist/server/main.js",
6 | "exec_mode": "cluster",
7 | "instances": "max"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { pathsToModuleNameMapper } = require('ts-jest');
2 | const { compilerOptions } = require('get-tsconfig').getTsconfig('./tsconfig.json')['config'];
3 |
4 | module.exports = {
5 | /* Basic settings */
6 | // Enable verbosity
7 | forceExit: false,
8 | verbose: true,
9 | // The root directory that Jest should scan for tests and modules within
10 | rootDir: './',
11 | // A list of paths to directories that Jest should use to search for files in
12 | roots: [''],
13 | testEnvironmentOptions: {
14 | NODE_ENV: 'test'
15 | },
16 | testMatch: ['/src/**/?(*.)+(unit|int|spec|test).(ts|js)'],
17 | preset: 'jest-preset-angular',
18 | // Resolve 'paths' from tsconfig.json
19 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }),
20 | // Ignore paths and modules
21 | modulePathIgnorePatterns: ['/dist'],
22 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
23 |
24 | /* Bootstrap settings */
25 | // Set initial config and enable jest-extended features
26 | globalSetup: 'jest-preset-angular/global-setup',
27 | setupFilesAfterEnv: ['/src/jest.setup.ts', 'jest-extended/all'],
28 |
29 | /* Global test settings */
30 | // Automatically clear mock calls and instances between every test
31 | clearMocks: true,
32 |
33 | /* Coverage settings */
34 | collectCoverage: false,
35 | // The directory where Jest should output its coverage files
36 | coverageDirectory: 'coverage',
37 | // An array of glob patterns indicating a set of files for which coverage information should be collected
38 | collectCoverageFrom: ['/src/**/*.ts'],
39 | coveragePathIgnorePatterns: ['/node_modules', '/src/types'],
40 | // Jest custom reporters
41 | reporters: ['default']
42 | /* Uncomment if you want to set thresholds for code coverage
43 | coverageThreshold: {
44 | global: {
45 | branches: 100,
46 | functions: 100,
47 | lines: 100,
48 | statements: 100
49 | }
50 | }
51 | */
52 | };
53 |
--------------------------------------------------------------------------------
/ngsw-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json",
3 | "appData": {
4 | "name": "Angular Skeleton"
5 | },
6 | "index": "/index.html",
7 | "assetGroups": [
8 | {
9 | "name": "app",
10 | "installMode": "prefetch",
11 | "updateMode": "prefetch",
12 | "resources": {
13 | "files": [
14 | "/favicon.ico",
15 | "/favicon.png",
16 | "/index.html",
17 | "/manifest.webmanifest",
18 | "/*.css",
19 | "/*.js",
20 | "/assets/i18n/**"
21 | ]
22 | }
23 | },
24 | {
25 | "name": "assets",
26 | "installMode": "lazy",
27 | "updateMode": "prefetch",
28 | "resources": {
29 | "files": ["/assets/**", "!/assets/i18n/**", "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"]
30 | }
31 | },
32 | {
33 | "name": "fonts",
34 | "installMode": "prefetch",
35 | "updateMode": "prefetch",
36 | "resources": {
37 | "files": ["/*.eot", "/*.svg", "/*.woff", "/*.woff2", "/*.ttf"],
38 | "urls": ["https://fonts.googleapis.com/**", "https://fonts.gstatic.com/**"]
39 | }
40 | }
41 | ],
42 | "dataGroups": [
43 | {
44 | "name": "api-performance",
45 | "urls": ["https://ui-avatars.com/api/**"],
46 | "cacheConfig": {
47 | "strategy": "performance",
48 | "maxSize": 100,
49 | "maxAge": "1d",
50 | "cacheOpaqueResponses": true
51 | }
52 | },
53 | {
54 | "name": "api-freshness",
55 | "urls": ["https://api.chucknorris.io/**"],
56 | "cacheConfig": {
57 | "strategy": "freshness",
58 | "maxSize": 100,
59 | "maxAge": "7h",
60 | "timeout": "0u"
61 | }
62 | }
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/nyc.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'report-dir': 'coverage-e2e',
3 | extension: ['.ts'],
4 | require: ['ts-node/register']
5 | };
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-skeleton",
3 | "version": "1.3.0",
4 | "description": "Angular Skeleton",
5 | "author": "Borja Paz Rodríguez (@borjapazr)",
6 | "license": "MIT",
7 | "keywords": [
8 | "frontend",
9 | "angular",
10 | "skeleton"
11 | ],
12 | "repository": "https://github.com/borjapazr/angular-skeleton",
13 | "scripts": {
14 | "start:dev": "ng serve --configuration development --port 4200 --open",
15 | "start:prod": "ng serve --configuration production --port 4300 --open",
16 | "start:ssr:dev": "ng run angular-skeleton:serve-ssr:development --port 4201 --open",
17 | "start:ssr:prod": "ng run angular-skeleton:serve-ssr:production --port 4301 --open",
18 | "build:dev": "rimraf dist && ng build --configuration development",
19 | "build:prod": "rimraf dist && ng build --configuration production && npm run build:optimize",
20 | "build:ssr:dev": "rimraf dist && ng build --configuration development && ng run angular-skeleton:server:development",
21 | "build:ssr:prod": "rimraf dist && ng build --configuration production && ng run angular-skeleton:server:production && npm run build:optimize",
22 | "build:prerender:dev": "rimraf dist && ng run angular-skeleton:prerender:development",
23 | "build:prerender:prod": "rimraf dist && ng run angular-skeleton:prerender:production && npm run build:optimize",
24 | "build:optimize": "run-s optimize:* && ngsw-config dist/browser ./ngsw-config.json",
25 | "optimize:i18n": "transloco-optimize dist/browser/assets/i18n",
26 | "serve:pwa": "http-server -p 4400 -P http://localhost:4400? dist/browser -o",
27 | "serve:ssr": "node dist/server/main.js",
28 | "i18n:extract": "transloco-keys-manager extract",
29 | "i18n:find": "transloco-keys-manager find",
30 | "check:types": "tsc --pretty --noEmit && tsc --project cypress/tsconfig.json --pretty --noEmit",
31 | "check:format": "prettier --check .",
32 | "check:lint": "eslint . --ext .js,.ts --color",
33 | "check:html": "htmlhint .",
34 | "check:scss": "stylelint 'src/**/*.{css,scss}' --color",
35 | "check:spelling": "cspell --config=.cspell.json \"{README.md,TODO.md,.github/*.md,src/**/*.ts,src/**/*.json}\"",
36 | "check:i18n": "transloco-validator src/assets/i18n/*.json",
37 | "check:staged": "lint-staged",
38 | "fix:format": "prettier --check --write --ignore-unknown .",
39 | "fix:lint": "npm run check:lint -- --fix",
40 | "fix:scss": "npm run check:scss -- --fix",
41 | "test": "cross-env NODE_ENV=test jest --verbose --colors --runInBand",
42 | "test:spec": "npm run test -- --testPathPattern=spec",
43 | "test:unit": "npm run test -- --testPathPattern=unit",
44 | "test:int": "npm run test -- --testPathPattern=integration",
45 | "e2e": "ng e2e",
46 | "e2e:run": "ng run angular-skeleton:cypress-run",
47 | "e2e:open": "ng run angular-skeleton:cypress-open",
48 | "e2e:coverage:view": "http-server -p 9004 ./coverage-e2e/lcov-report -o",
49 | "test:watch": "npm run test -- --watch",
50 | "test:coverage": "npm run test -- --coverage --silent",
51 | "test:coverage:view": "http-server -p 9003 ./coverage/lcov-report -o",
52 | "reset-hard": "git clean -dfx && git reset --hard && npm install",
53 | "version": "standard-version -t",
54 | "prepare-release": "run-s reset-hard version",
55 | "commit": "cz",
56 | "update-deps": "npm-check-updates -u",
57 | "prepare": "husky install"
58 | },
59 | "dependencies": {
60 | "@angular/animations": "^15.2.3",
61 | "@angular/common": "^15.2.3",
62 | "@angular/compiler": "^15.2.3",
63 | "@angular/core": "^15.2.3",
64 | "@angular/forms": "^15.2.3",
65 | "@angular/platform-browser": "^15.2.3",
66 | "@angular/platform-browser-dynamic": "^15.2.3",
67 | "@angular/platform-server": "^15.2.3",
68 | "@angular/router": "^15.2.3",
69 | "@angular/service-worker": "^15.2.3",
70 | "@ngneat/transloco": "^4.2.6",
71 | "@ngneat/transloco-locale": "^4.0.0",
72 | "@ngneat/transloco-messageformat": "^4.1.0",
73 | "@ngneat/transloco-preload-langs": "^4.0.1",
74 | "@ngneat/until-destroy": "^9.2.3",
75 | "@nguniversal/express-engine": "^15.2.0",
76 | "compression": "^1.7.4",
77 | "cookie-parser": "^1.4.6",
78 | "domino": "^2.1.6",
79 | "dotenv-defaults": "^5.0.2",
80 | "dotenv-expand": "^10.0.0",
81 | "endent": "^2.1.0",
82 | "express": "^4.18.2",
83 | "fast-deep-equal": "^3.1.3",
84 | "figlet": "^1.5.2",
85 | "http-status-codes": "^2.2.0",
86 | "intersection-observer": "^0.12.2",
87 | "just-is-empty": "^3.3.0",
88 | "messageformat": "^2.3.0",
89 | "ng-lazyload-image": "^9.1.3",
90 | "ngx-hover-preload": "^0.0.3",
91 | "ngx-isr": "^0.5.1",
92 | "ngx-quicklink": "0.4.1",
93 | "node-fetch": "^3.3.1",
94 | "rxjs": "~7.8.0",
95 | "tslib": "^2.5.0",
96 | "zone.js": "~0.13.0"
97 | },
98 | "devDependencies": {
99 | "@angular-builders/jest": "^15.0.0",
100 | "@angular-devkit/build-angular": "^15.2.3",
101 | "@angular-eslint/builder": "15.2.1",
102 | "@angular-eslint/eslint-plugin": "15.2.1",
103 | "@angular-eslint/eslint-plugin-template": "15.2.1",
104 | "@angular-eslint/schematics": "15.2.1",
105 | "@angular-eslint/template-parser": "15.2.1",
106 | "@angular/cli": "~15.2.3",
107 | "@angular/compiler-cli": "^15.2.3",
108 | "@commitlint/cli": "^17.4.4",
109 | "@commitlint/config-conventional": "^17.4.4",
110 | "@cspell/dict-bash": "^4.1.1",
111 | "@cspell/dict-companies": "^3.0.9",
112 | "@cspell/dict-es-es": "^2.2.4",
113 | "@cspell/dict-filetypes": "^3.0.0",
114 | "@cspell/dict-html": "^4.0.3",
115 | "@cspell/dict-lorem-ipsum": "^3.0.0",
116 | "@cspell/dict-node": "^4.0.2",
117 | "@cspell/dict-npm": "^5.0.5",
118 | "@cspell/dict-software-terms": "^3.1.6",
119 | "@cspell/dict-typescript": "^3.1.1",
120 | "@cypress/code-coverage": "^3.10.0",
121 | "@cypress/schematic": "^2.5.0",
122 | "@ngneat/transloco-keys-manager": "^3.6.2",
123 | "@ngneat/transloco-optimize": "^3.0.2",
124 | "@ngneat/transloco-validator": "^3.0.1",
125 | "@nguniversal/builders": "^15.2.0",
126 | "@skyux-sdk/istanbul-instrumenter-loader": "^4.0.0",
127 | "@tailwindcss/aspect-ratio": "^0.4.2",
128 | "@tailwindcss/forms": "^0.5.3",
129 | "@tailwindcss/typography": "^0.5.9",
130 | "@types/compression": "^1.7.2",
131 | "@types/cookie-parser": "^1.4.3",
132 | "@types/dotenv-defaults": "^2.0.1",
133 | "@types/express": "^4.17.17",
134 | "@types/figlet": "^1.5.5",
135 | "@types/jest": "^28.1.3",
136 | "@types/node": "^14.15.0",
137 | "@typescript-eslint/eslint-plugin": "^5.56.0",
138 | "@typescript-eslint/parser": "^5.56.0",
139 | "autoprefixer": "^10.4.14",
140 | "commitizen": "^4.3.0",
141 | "cross-env": "^7.0.3",
142 | "cspell": "^6.30.2",
143 | "cypress": "^12.8.1",
144 | "cz-conventional-changelog": "^3.3.0",
145 | "eslint": "^8.36.0",
146 | "eslint-config-prettier": "^8.8.0",
147 | "eslint-import-resolver-typescript": "^3.5.3",
148 | "eslint-plugin-cypress": "^2.12.1",
149 | "eslint-plugin-deprecation": "^1.3.3",
150 | "eslint-plugin-eslint-comments": "^3.2.0",
151 | "eslint-plugin-import": "^2.27.5",
152 | "eslint-plugin-jest": "^27.2.1",
153 | "eslint-plugin-prefer-arrow": "^1.2.3",
154 | "eslint-plugin-prettier": "^4.2.1",
155 | "eslint-plugin-simple-import-sort": "^10.0.0",
156 | "eslint-plugin-sonarjs": "^0.19.0",
157 | "eslint-plugin-unused-imports": "^2.0.0",
158 | "get-tsconfig": "^4.4.0",
159 | "htmlhint": "^1.1.4",
160 | "http-server": "^14.1.1",
161 | "husky": "^8.0.3",
162 | "jest": "^28.1.3",
163 | "jest-extended": "^3.2.4",
164 | "jest-preset-angular": "^12.2.5",
165 | "lint-staged": "^13.2.0",
166 | "ngx-build-plus": "^15.0.0",
167 | "npm-check-updates": "^16.7.13",
168 | "npm-run-all": "^4.1.5",
169 | "postcss": "^8.4.21",
170 | "prettier": "^2.8.6",
171 | "prettier-eslint": "^15.0.1",
172 | "prettier-plugin-tailwindcss": "^0.2.5",
173 | "rimraf": "^4.4.0",
174 | "standard-version": "^9.5.0",
175 | "stylelint": "^15.3.0",
176 | "stylelint-config-recommended-scss": "^9.0.1",
177 | "stylelint-config-standard": "^31.0.0",
178 | "stylelint-config-standard-scss": "^7.0.1",
179 | "stylelint-no-unsupported-browser-features": "^6.1.0",
180 | "stylelint-prettier": "^3.0.0",
181 | "stylelint-scss": "^4.5.0",
182 | "tailwindcss": "^3.2.7",
183 | "ts-jest": "^28.0.8",
184 | "tsc-alias": "^1.8.4",
185 | "typescript": "~4.9.5"
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/routes.txt:
--------------------------------------------------------------------------------
1 | /
2 | /es
3 | /es/welcome
4 | /es/jokes
5 | /es/jokes/chuck-norris
6 | /en
7 | /en/welcome
8 | /en/jokes
9 | /en/jokes/chuck-norris
10 |
--------------------------------------------------------------------------------
/server.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/dist/zone-node';
2 |
3 | import { APP_BASE_HREF } from '@angular/common';
4 | import { ngExpressEngine } from '@nguniversal/express-engine';
5 | import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
6 | import * as compression from 'compression';
7 | import * as cookieParser from 'cookie-parser';
8 | import * as domino from 'domino';
9 | import * as dotEnvConfig from 'dotenv-defaults/config';
10 | import * as dotenvExpand from 'dotenv-expand';
11 | import endent from 'endent';
12 | import express, { Express } from 'express';
13 | import figlet from 'figlet';
14 | import standard from 'figlet/importable-fonts/Standard';
15 | import { existsSync, readFileSync } from 'fs';
16 | import { ISRHandler } from 'ngx-isr';
17 | import fetch from 'node-fetch';
18 | import { join } from 'path';
19 |
20 | import { Logger } from '@core/services/logger.service';
21 | import { environment } from '@env/environment';
22 |
23 | dotenvExpand.expand(dotEnvConfig);
24 |
25 | const LOGGER = new Logger('server');
26 |
27 | const PORT = process.env['PORT'] || 4000;
28 | const ISR_TOKEN = process.env['ISR_TOKEN'] || 'angular-skeleton';
29 | const DIST_FOLDER = join(process.cwd(), 'dist/browser');
30 | const INDEX_HTML = existsSync(join(DIST_FOLDER, 'index.original.html')) ? 'index.original.html' : 'index';
31 | const TEMPLATE = readFileSync(join(DIST_FOLDER, 'index.html')).toString();
32 |
33 | /**
34 | * window and document polyfills
35 | */
36 | // Server-side DOM implementation
37 | const window: any = domino.createWindow(TEMPLATE);
38 | window.Object = Object;
39 | window.Math = Math;
40 | window.fetch = fetch;
41 |
42 | // Browser objects abstractions
43 | (global as any).window = window;
44 | Object.defineProperty(window.document.body.style, 'transform', {
45 | value: () => {
46 | return {
47 | configurable: true,
48 | enumerable: true
49 | };
50 | }
51 | });
52 | (global as any).document = window.document;
53 | (global as any).HTMLElement = window.HTMLElement;
54 | (global as any).HTMLElement.prototype.getBoundingClientRect = () => {
55 | return {
56 | left: '',
57 | right: '',
58 | top: '',
59 | bottom: ''
60 | };
61 | };
62 |
63 | // Other optional depending on application configuration
64 | (global as any).object = window.object;
65 | (global as any).navigator = window.navigator;
66 | (global as any).localStorage = window.localStorage;
67 | (global as any).sessionStorage = window.sessionStorage;
68 | (global as any).DOMTokenList = window.DOMTokenList;
69 |
70 | // eslint-disable-next-line import/first
71 | import { AppServerModule } from './src/main.server';
72 |
73 | const app = (): Express => {
74 | const server = express();
75 |
76 | server.use(compression());
77 | server.use(express.json());
78 | server.use(express.urlencoded({ extended: true }));
79 | server.use(cookieParser());
80 |
81 | server.engine(
82 | 'html',
83 | ngExpressEngine({
84 | bootstrap: AppServerModule
85 | })
86 | );
87 | server.set('view engine', 'html');
88 | server.set('views', DIST_FOLDER);
89 |
90 | // Health check
91 | server.get('/healthz', (_req, res) =>
92 | res.json({
93 | status: 'ALIVE',
94 | message: `🚀 To infinity and beyond!`
95 | })
96 | );
97 |
98 | // Invalidate cached routes
99 | server.get('/invalidate', async (req, res) => await isr.invalidate(req, res));
100 |
101 | // Serve static files from /browser
102 | server.get(
103 | '*.*',
104 | express.static(DIST_FOLDER, {
105 | maxAge: '1y'
106 | })
107 | );
108 |
109 | // All regular routes use the ISR Handler
110 | const isr = new ISRHandler({
111 | indexHtml: INDEX_HTML,
112 | invalidateSecretToken: ISR_TOKEN,
113 | enableLogging: !environment.production
114 | });
115 |
116 | server.get(
117 | '*',
118 | (req, _, next) => {
119 | (global as any).navigator = { userAgent: req['headers']['user-agent'] } as Navigator;
120 | next();
121 | },
122 | // Serve page if it exists in cache
123 | async (req, res, next) => await isr.serveFromCache(req, res, next),
124 | // Server side render the page and add to cache if needed
125 | async (req, res, next) =>
126 | await isr.render(req, res, next, {
127 | providers: [
128 | { provide: APP_BASE_HREF, useValue: req.baseUrl },
129 | { provide: REQUEST, useValue: req },
130 | { provide: RESPONSE, useValue: res }
131 | ]
132 | })
133 | );
134 |
135 | return server;
136 | };
137 |
138 | const run = (): void => {
139 | app().listen(PORT, () => showBanner());
140 | };
141 |
142 | const showBanner = (): void => {
143 | figlet.parseFont('Standard', standard);
144 |
145 | const banner = endent`Application started successfully!
146 | ${figlet.textSync(environment.appName)}
147 | Name: 🧱 ${environment.appName}
148 | Description: 📖 ${environment.appDescription}
149 | Port: ${PORT}
150 | Base Path: /
151 | Environment: ${environment.production ? 'production' : 'development'}
152 | Author: ${environment.author}
153 | Email: ${environment.authorEmail}
154 | Website: ${environment.authorWebsite}
155 | Copyright © ${new Date().getFullYear()} ${environment.author}. All rights reserved.
156 | `;
157 | LOGGER.info(banner);
158 | };
159 |
160 | // Webpack will replace 'require' with '__webpack_require__'
161 | // '__non_webpack_require__' is a proxy to Node 'require'
162 | // The below code is to ensure that the server is run only when not requiring the bundle.
163 | // eslint-disable-next-line no-undef
164 | declare const __non_webpack_require__: NodeRequire;
165 | const mainModule = __non_webpack_require__.main;
166 | const moduleFilename = (mainModule && mainModule.filename) || '';
167 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
168 | run();
169 | }
170 |
171 | export { app };
172 | export * from './src/main.server';
173 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes, UrlMatcher, UrlSegment } from '@angular/router';
3 |
4 | import { LANGUAGES } from './core/constants';
5 | import { localizeRoute } from './core/guards';
6 | import { languageResolver } from './core/resolvers';
7 | import { CustomRoutePreloadingStrategy } from './core/strategies';
8 | import { ErrorComponent } from './shared/components';
9 |
10 | const languageMatcher: UrlMatcher = (url: UrlSegment[]) => {
11 | const isAllowedLanguage =
12 | Array.isArray(url) && url.length && LANGUAGES.map(language => language.id).includes(url[0].path);
13 | return isAllowedLanguage ? { consumed: url.slice(0, 1) } : null;
14 | };
15 |
16 | /**
17 | * t(routes.default.title, routes.default.description)
18 | * t(routes.welcome.title, routes.welcome.description)
19 | * t(routes.jokes.title, routes.jokes.description)
20 | */
21 | const routes: Routes = [
22 | { path: '', redirectTo: 'welcome', pathMatch: 'full' },
23 | {
24 | path: 'welcome',
25 | loadChildren: () => import('./features/home/home.module').then(m => m.HomeModule),
26 | data: {
27 | meta: {
28 | titleKey: 'routes.welcome.title',
29 | descriptionKey: 'routes.welcome.description'
30 | }
31 | }
32 | },
33 | {
34 | path: 'jokes',
35 | loadChildren: () => import('./features/jokes/jokes.module').then(m => m.JokesModule),
36 | data: {
37 | preload: true,
38 | delayInSeconds: 10,
39 | meta: {
40 | titleKey: 'routes.jokes.title',
41 | descriptionKey: 'routes.jokes.description'
42 | }
43 | }
44 | },
45 | {
46 | path: '**',
47 | component: ErrorComponent,
48 | data: {
49 | revalidate: 0
50 | }
51 | }
52 | ];
53 |
54 | @NgModule({
55 | imports: [
56 | RouterModule.forRoot(
57 | [
58 | {
59 | matcher: languageMatcher,
60 | children: routes,
61 | resolve: [languageResolver]
62 | },
63 | { path: '', canActivate: [localizeRoute], children: routes }
64 | ],
65 | {
66 | initialNavigation: 'enabledBlocking',
67 | scrollPositionRestoration: 'enabled',
68 | preloadingStrategy: CustomRoutePreloadingStrategy
69 | }
70 | )
71 | ],
72 | exports: [RouterModule]
73 | })
74 | export class AppRoutingModule {}
75 |
--------------------------------------------------------------------------------
/src/app/app.browser.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ServiceWorkerModule } from '@angular/service-worker';
3 | import { TRANSLOCO_LOADER } from '@ngneat/transloco';
4 | import { TranslocoPreloadLangsModule } from '@ngneat/transloco-preload-langs';
5 | import { StateTransferInitializerModule } from '@nguniversal/common';
6 |
7 | import { LANGUAGES } from '@core/constants';
8 | import { I18nBrowserLoader } from '@core/loaders/i18n-browser.loader';
9 | import { environment } from '@env/environment';
10 |
11 | import { AppComponent } from './app.component';
12 | import { AppModule } from './app.module';
13 |
14 | @NgModule({
15 | imports: [
16 | // Application NgModule
17 | AppModule,
18 |
19 | // SSR - Angular Universal
20 | StateTransferInitializerModule,
21 |
22 | // i18n
23 | TranslocoPreloadLangsModule.forRoot(LANGUAGES.map(language => language.id)),
24 |
25 | // PWA - Service Worker
26 | ServiceWorkerModule.register('ngsw-worker.js', {
27 | enabled: environment.production,
28 | registrationStrategy: 'registerWhenStable:30000'
29 | })
30 | ],
31 | providers: [{ provide: TRANSLOCO_LOADER, useClass: I18nBrowserLoader }],
32 | bootstrap: [AppComponent]
33 | })
34 | export class AppBrowserModule {}
35 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/app.component.scss
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss']
7 | })
8 | export class AppComponent {
9 | constructor() {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { TransferHttpCacheModule } from '@nguniversal/common';
4 |
5 | import { CoreModule } from '@core/core.module';
6 |
7 | import { AppComponent } from './app.component';
8 | import { AppRoutingModule } from './app-routing.module';
9 | import { SharedModule } from './shared/shared.module';
10 |
11 | @NgModule({
12 | declarations: [AppComponent],
13 | imports: [
14 | // SSR - Angular Universal
15 | BrowserModule.withServerTransition({ appId: 'angular-skeleton' }),
16 | TransferHttpCacheModule,
17 |
18 | // Application modules
19 | CoreModule.forRoot(),
20 | SharedModule.forRoot(),
21 |
22 | // Application routing
23 | AppRoutingModule
24 | ],
25 | providers: [],
26 | bootstrap: [AppComponent]
27 | })
28 | export class AppModule {}
29 |
--------------------------------------------------------------------------------
/src/app/app.server.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ServerModule } from '@angular/platform-server';
3 | import { TRANSLOCO_LOADER } from '@ngneat/transloco';
4 | import { NgxIsrModule } from 'ngx-isr';
5 |
6 | import { I18nServerLoader } from '@core/loaders/i18n-server.loader';
7 |
8 | import { AppComponent } from './app.component';
9 | import { AppModule } from './app.module';
10 |
11 | @NgModule({
12 | imports: [
13 | // Application NgModule
14 | AppModule,
15 |
16 | // SSR - Angular Universal
17 | ServerModule,
18 | NgxIsrModule.forRoot()
19 | ],
20 | providers: [{ provide: TRANSLOCO_LOADER, useClass: I18nServerLoader }],
21 | bootstrap: [AppComponent]
22 | })
23 | export class AppServerModule {}
24 |
--------------------------------------------------------------------------------
/src/app/core/components/content/content.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/core/components/content/content.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/core/components/content/content.component.scss
--------------------------------------------------------------------------------
/src/app/core/components/content/content.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 |
4 | import { ContentComponent } from './content.component';
5 |
6 | describe('Core -> ContentComponent', () => {
7 | let component: ContentComponent;
8 | let fixture: ComponentFixture;
9 |
10 | beforeEach(async () => {
11 | await TestBed.configureTestingModule({
12 | declarations: [ContentComponent],
13 | imports: [RouterTestingModule]
14 | }).compileComponents();
15 | });
16 |
17 | beforeEach(() => {
18 | fixture = TestBed.createComponent(ContentComponent);
19 | component = fixture.componentInstance;
20 | fixture.detectChanges();
21 | });
22 |
23 | it('should create', () => {
24 | expect(component).toBeTruthy();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/app/core/components/content/content.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-content',
5 | templateUrl: './content.component.html',
6 | styleUrls: ['./content.component.scss']
7 | })
8 | export class ContentComponent {
9 | constructor() {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/core/components/footer/footer.component.html:
--------------------------------------------------------------------------------
1 |
2 |
27 |
--------------------------------------------------------------------------------
/src/app/core/components/footer/footer.component.scss:
--------------------------------------------------------------------------------
1 | :host footer {
2 | @apply flex;
3 |
4 | .left,
5 | .right {
6 | @apply flex-1;
7 | }
8 |
9 | .left {
10 | @apply hidden align-middle sm:inline-block;
11 |
12 | app-github-icon {
13 | @apply -mt-1 mr-2 hidden align-middle text-stone-900 dark:text-stone-100 sm:inline-block;
14 | }
15 |
16 | app-heart-icon {
17 | @apply -mt-1 hidden align-middle text-red-500 sm:inline-block;
18 | }
19 | }
20 |
21 | .center {
22 | .material-symbols-outlined {
23 | @apply mx-2 -mt-1 align-middle;
24 | }
25 |
26 | .author {
27 | @apply hidden md:inline-block;
28 | }
29 | }
30 |
31 | .right {
32 | @apply text-right;
33 |
34 | .material-symbols-outlined {
35 | @apply -mt-1 hidden align-middle sm:inline-block;
36 | }
37 |
38 | div {
39 | @apply ml-4 inline-block;
40 | }
41 |
42 | span {
43 | @apply cursor-pointer;
44 |
45 | &:last-child {
46 | @apply m-4;
47 | }
48 |
49 | &.active {
50 | @apply border-2 border-solid border-stone-600 px-3 py-1.5 font-bold border-sketch-2 dark:border-stone-100;
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/core/components/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | import { I18nService } from '@app/core/services';
4 | import { LANGUAGES } from '@core/constants';
5 | import { environment } from '@env/environment';
6 |
7 | @Component({
8 | selector: 'app-footer',
9 | templateUrl: './footer.component.html',
10 | styleUrls: ['./footer.component.scss']
11 | })
12 | export class FooterComponent {
13 | readonly author = environment.author;
14 |
15 | readonly year = new Date().getFullYear();
16 |
17 | readonly website = environment.authorWebsite;
18 |
19 | readonly languages = LANGUAGES;
20 |
21 | readonly activeLanguage$ = this.i18nService.getActiveLanguage$();
22 |
23 | constructor(private readonly i18nService: I18nService) {}
24 |
25 | public setActiveLanguage(language: string): void {
26 | this.i18nService.setActiveLanguage(language);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/core/components/header/header.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
{{ title }}
13 |
v{{ version }}
14 |
15 |
16 |
17 |
18 | dark_mode
19 | light_mode
20 |
21 |
22 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/app/core/components/header/header.component.scss:
--------------------------------------------------------------------------------
1 | header {
2 | @apply flex;
3 |
4 | .left,
5 | .right {
6 | @apply flex-1;
7 |
8 | button {
9 | @apply mr-4;
10 | }
11 |
12 | .material-symbols-outlined {
13 | @apply align-middle text-3xl;
14 | }
15 | }
16 |
17 | .left {
18 | @apply text-left;
19 |
20 | img {
21 | @apply -mt-4 mr-3 hidden w-12 cursor-pointer md:inline-block;
22 | }
23 |
24 | h1 {
25 | @apply mr-2 inline-block text-2xl md:text-4xl;
26 | }
27 | }
28 |
29 | .right {
30 | @apply text-right;
31 |
32 | img {
33 | @apply inline-block w-11 rounded-lg;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/core/components/header/header.component.ts:
--------------------------------------------------------------------------------
1 | import { DOCUMENT } from '@angular/common';
2 | import { Component, Inject } from '@angular/core';
3 |
4 | import { Cookie } from '@app/core/enums';
5 | import { StorageService } from '@app/shared/services';
6 | import { environment } from '@env/environment';
7 |
8 | @Component({
9 | selector: 'app-header',
10 | templateUrl: './header.component.html',
11 | styleUrls: ['./header.component.scss']
12 | })
13 | export class HeaderComponent {
14 | public readonly logo = '/assets/images/logo.svg';
15 |
16 | public readonly lightAvatar =
17 | 'https://ui-avatars.com/api/?background=222&color=f5f5f4&size=88&name=Borja+Paz&rounded=false&background=404040';
18 |
19 | public readonly darkAvatar =
20 | 'https://ui-avatars.com/api/?background=222&color=404040&size=88&name=Borja+Paz&rounded=false&background=f5f5f4';
21 |
22 | public readonly title = environment.appName;
23 |
24 | public readonly version = environment.appVersion;
25 |
26 | public isDarkMode = this.getIsDarkMode();
27 |
28 | constructor(@Inject(DOCUMENT) private readonly document: Document, private readonly storageService: StorageService) {
29 | this.setIsDarkMode(this.isDarkMode);
30 | }
31 |
32 | public toggleDarkMode(): void {
33 | this.isDarkMode = !this.isDarkMode;
34 | this.setIsDarkMode(this.isDarkMode);
35 | }
36 |
37 | private getIsDarkMode(): boolean {
38 | return this.storageService.getCookie(Cookie.DARK_MODE)
39 | ? this.storageService.getCookie(Cookie.DARK_MODE) === '1'
40 | : environment.darkModeAsDefault;
41 | }
42 |
43 | private setIsDarkMode(enabled: boolean): void {
44 | this.storageService.saveCookie(Cookie.DARK_MODE, this.isDarkMode ? '1' : '0', 90);
45 | this.document.documentElement.className = enabled ? 'dark' : '';
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/core/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './content/content.component';
2 | export * from './footer/footer.component';
3 | export * from './header/header.component';
4 | export * from './navbar/navbar.component';
5 |
--------------------------------------------------------------------------------
/src/app/core/components/navbar/navbar.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app/core/components/navbar/navbar.component.scss:
--------------------------------------------------------------------------------
1 | :host ul {
2 | @apply m-0 list-none;
3 |
4 | li {
5 | @apply px-3 py-2.5 text-center;
6 |
7 | &.active {
8 | @apply border-2 border-solid border-stone-600 px-3 py-2.5 font-bold border-sketch-2 dark:border-stone-100;
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/core/components/navbar/navbar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | import { MENU_ENTRIES } from '@core/constants';
4 |
5 | @Component({
6 | selector: 'app-navbar',
7 | templateUrl: './navbar.component.html',
8 | styleUrls: ['./navbar.component.scss']
9 | })
10 | export class NavbarComponent {
11 | readonly menuEntries = MENU_ENTRIES;
12 |
13 | constructor() {}
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/core/constants/index.ts:
--------------------------------------------------------------------------------
1 | export * from './languages';
2 | export * from './menu-entries';
3 |
--------------------------------------------------------------------------------
/src/app/core/constants/languages.ts:
--------------------------------------------------------------------------------
1 | type Language = { id: string; name: string; locale: string };
2 |
3 | const LANGUAGES: Language[] = [
4 | { id: 'en', name: 'English', locale: 'en-US' },
5 | { id: 'es', name: 'Spanish', locale: 'es-ES' }
6 | ];
7 |
8 | export { Language, LANGUAGES };
9 |
--------------------------------------------------------------------------------
/src/app/core/constants/menu-entries.ts:
--------------------------------------------------------------------------------
1 | type MenuEntry = { labelId: string; link: string | string[] };
2 |
3 | const MENU_ENTRIES: MenuEntry[] = [
4 | {
5 | labelId: 'welcome',
6 | link: '/welcome'
7 | },
8 | {
9 | labelId: 'jokes',
10 | link: ['/', 'jokes', 'chuck-norris']
11 | },
12 | {
13 | labelId: 'notFound',
14 | link: '/non-existent'
15 | }
16 | ];
17 |
18 | export { MENU_ENTRIES, MenuEntry };
19 |
--------------------------------------------------------------------------------
/src/app/core/core.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
3 | import { APP_INITIALIZER, ErrorHandler, ModuleWithProviders, NgModule, Optional, SkipSelf, Type } from '@angular/core';
4 | import { RouteReuseStrategy, RouterModule, TitleStrategy } from '@angular/router';
5 | import { TRANSLOCO_CONFIG, translocoConfig, TranslocoModule } from '@ngneat/transloco';
6 | import { TranslocoLocaleModule } from '@ngneat/transloco-locale';
7 | import { TranslocoMessageFormatModule } from '@ngneat/transloco-messageformat';
8 | import { LAZYLOAD_IMAGE_HOOKS } from 'ng-lazyload-image';
9 |
10 | import { LANGUAGES } from '@core/constants/languages';
11 | import { environment } from '@env/environment';
12 | import { SharedModule } from '@shared/shared.module';
13 |
14 | import { ContentComponent, FooterComponent, HeaderComponent, NavbarComponent } from './components';
15 | import { EnsureModuleLoadedOnce } from './guards';
16 | import { GlobalErrorHandler } from './handlers';
17 | import { HttpErrorInterceptor } from './interceptors';
18 | import { AppInitializer } from './services';
19 | import { AppUpdater } from './services/app-updater.service';
20 | import { CustomPageTitleStrategy, CustomRouteReuseStrategy } from './strategies';
21 | import { CustomLazyLoadImageStrategy } from './strategies/custom-lazyload-image.strategy';
22 | import { APP_NAME } from './tokens/app-name';
23 |
24 | const SHARED_ITEMS: Type[] = [FooterComponent, HeaderComponent, ContentComponent, NavbarComponent];
25 |
26 | const initializeApplication = (appInitializer: AppInitializer) => (): Promise => appInitializer.initialize();
27 |
28 | const availableLanguages = LANGUAGES.map(language => ({ id: language.id, label: language.name }));
29 | const languagesToLocales = Object.assign({}, ...LANGUAGES.map(language => ({ [language.id]: language.locale })));
30 |
31 | @NgModule({
32 | declarations: [...SHARED_ITEMS],
33 | imports: [
34 | // Angular modules
35 | CommonModule,
36 | RouterModule,
37 | HttpClientModule,
38 |
39 | // i18n
40 | TranslocoMessageFormatModule.forRoot({
41 | enableCache: true
42 | }),
43 | TranslocoLocaleModule.forRoot({
44 | defaultLocale: languagesToLocales[environment.defaultLanguage],
45 | langToLocaleMapping: languagesToLocales
46 | }),
47 | TranslocoModule,
48 |
49 | // Shared modules
50 | SharedModule
51 | ],
52 | exports: [HttpClientModule, ...SHARED_ITEMS]
53 | })
54 | export class CoreModule extends EnsureModuleLoadedOnce {
55 | constructor(@Optional() @SkipSelf() parentModule: CoreModule, private appUpdater: AppUpdater) {
56 | super(parentModule as NgModule);
57 | this.appUpdater.handleAppUpdates();
58 | }
59 |
60 | static forRoot(): ModuleWithProviders {
61 | return {
62 | ngModule: CoreModule,
63 | providers: [
64 | {
65 | provide: APP_NAME,
66 | useValue: environment.appName
67 | },
68 | {
69 | provide: APP_INITIALIZER,
70 | useFactory: initializeApplication,
71 | deps: [AppInitializer],
72 | multi: true
73 | },
74 | {
75 | provide: ErrorHandler,
76 | useClass: GlobalErrorHandler
77 | },
78 | {
79 | provide: HTTP_INTERCEPTORS,
80 | useClass: HttpErrorInterceptor,
81 | multi: true
82 | },
83 | {
84 | provide: TRANSLOCO_CONFIG,
85 | useValue: translocoConfig({
86 | availableLangs: availableLanguages,
87 | defaultLang: environment.defaultLanguage || 'en',
88 | flatten: {
89 | aot: environment.production
90 | },
91 | prodMode: environment.production,
92 | reRenderOnLangChange: true,
93 | fallbackLang: environment.defaultLanguage || 'en'
94 | })
95 | },
96 | {
97 | provide: RouteReuseStrategy,
98 | useClass: CustomRouteReuseStrategy
99 | },
100 | {
101 | provide: TitleStrategy,
102 | useClass: CustomPageTitleStrategy
103 | },
104 | {
105 | provide: LAZYLOAD_IMAGE_HOOKS,
106 | useClass: CustomLazyLoadImageStrategy
107 | }
108 | ]
109 | };
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/app/core/enums/cookie.ts:
--------------------------------------------------------------------------------
1 | export enum Cookie {
2 | LANGUAGE = 'language',
3 | DARK_MODE = 'dark_mode'
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/core/enums/header.ts:
--------------------------------------------------------------------------------
1 | export enum Header {
2 | ACCEPT_LANGUAGE = 'accept-language'
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/core/enums/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cookie';
2 | export * from './header';
3 | export * from './transfer-state-key';
4 |
--------------------------------------------------------------------------------
/src/app/core/enums/transfer-state-key.ts:
--------------------------------------------------------------------------------
1 | export enum TransferStateKey {
2 | TRANSLATIONS = 'transfer-translate'
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/core/guards/ensure-module-loaded-once.guard.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | class EnsureModuleLoadedOnce {
4 | constructor(targetModule: NgModule) {
5 | if (targetModule) {
6 | throwIfAlreadyLoaded(targetModule);
7 | }
8 | }
9 | }
10 |
11 | const throwIfAlreadyLoaded = (parentModule: NgModule): void => {
12 | if (parentModule) {
13 | throw new Error(
14 | `${parentModule.constructor.name} has already been loaded. Import this module in the AppModule only.`
15 | );
16 | }
17 | };
18 |
19 | export { EnsureModuleLoadedOnce, throwIfAlreadyLoaded };
20 |
--------------------------------------------------------------------------------
/src/app/core/guards/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ensure-module-loaded-once.guard';
2 | export * from './localize-route.guard';
3 |
--------------------------------------------------------------------------------
/src/app/core/guards/localize-route.guard.ts:
--------------------------------------------------------------------------------
1 | import { inject } from '@angular/core';
2 | import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
3 | import isEmpty from 'just-is-empty';
4 | import { Observable } from 'rxjs';
5 |
6 | import { PlatformService } from '@core/services';
7 | import { I18nService } from '@core/services/i18n.service';
8 | import { BrowserService, ServerService } from '@shared/services';
9 |
10 | const localizeRoute = (
11 | _route: ActivatedRouteSnapshot,
12 | state: RouterStateSnapshot,
13 | router: Router = inject(Router),
14 | i18nService: I18nService = inject(I18nService),
15 | platformService: PlatformService = inject(PlatformService),
16 | browserService: BrowserService = inject(BrowserService),
17 | serverService: ServerService = inject(ServerService)
18 | ): Observable | Promise | boolean | UrlTree => {
19 | const urlLanguage = i18nService.getUrlLanguage(state.url);
20 |
21 | if (isEmpty(urlLanguage)) {
22 | const availableLanguages = i18nService.getAvailableLanguages();
23 | const cookieLanguage = i18nService.getCookieLanguage();
24 | const browserLanguage = browserService.getBrowserLanguage() ?? serverService.getLanguageHeader();
25 |
26 | const targetLanguage = [cookieLanguage, browserLanguage, i18nService.getDefaultLanguage()].find(language =>
27 | availableLanguages.some(availableLanguage => availableLanguage.id === language)
28 | );
29 |
30 | if (platformService.isBrowser) {
31 | /**
32 | * It is implemented in this way until this issue is resolved.
33 | * https://github.com/angular/angular/issues/27148
34 | */
35 | router.navigateByUrl(`${targetLanguage}${state.url}`, { replaceUrl: true });
36 | return false;
37 | }
38 | serverService.setRedirectResponse(`/${targetLanguage}${state.url}`, false);
39 | }
40 |
41 | return true;
42 | };
43 |
44 | export { localizeRoute };
45 |
--------------------------------------------------------------------------------
/src/app/core/handlers/global-error.handler.ts:
--------------------------------------------------------------------------------
1 | import { HttpErrorResponse } from '@angular/common/http';
2 | import { ErrorHandler, Injectable } from '@angular/core';
3 |
4 | import { Logger } from '@core/services';
5 | import { environment } from '@env/environment';
6 |
7 | const LOGGER = new Logger('GlobalErrorHandler');
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class GlobalErrorHandler implements ErrorHandler {
13 | constructor() {}
14 |
15 | handleError(error: Error | HttpErrorResponse) {
16 | if (error instanceof HttpErrorResponse) {
17 | this.handleServerError(error);
18 | } else {
19 | this.handleClientError(error);
20 | }
21 | }
22 |
23 | private handleServerError(error: HttpErrorResponse) {
24 | if (!navigator.onLine) {
25 | LOGGER.error('No Internet Connection');
26 | }
27 |
28 | if (!environment.production) {
29 | LOGGER.error('Http Error', error);
30 | }
31 | }
32 |
33 | private handleClientError(error: Error) {
34 | LOGGER.error(error);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/core/handlers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './global-error.handler';
2 |
--------------------------------------------------------------------------------
/src/app/core/interceptors/http-error.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { catchError, Observable, retry } from 'rxjs';
4 |
5 | import { AppBusService } from '@core/services';
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class HttpErrorInterceptor implements HttpInterceptor {
11 | constructor(private readonly appBusService: AppBusService) {}
12 |
13 | intercept(request: HttpRequest, next: HttpHandler): Observable> {
14 | return next.handle(request).pipe(
15 | retry(1),
16 | catchError(error => this.handlerError(error))
17 | );
18 | }
19 |
20 | private handlerError(error: HttpErrorResponse): Observable> {
21 | this.appBusService.emitHttpError(error);
22 | throw error;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/core/interceptors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './http-error.interceptor';
2 |
--------------------------------------------------------------------------------
/src/app/core/loaders/i18n-browser.loader.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { makeStateKey, StateKey, TransferState } from '@angular/platform-browser';
4 | import { Translation, TranslocoLoader } from '@ngneat/transloco';
5 | import { Observable, Observer } from 'rxjs';
6 |
7 | import { TransferStateKey } from '@core/enums';
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class I18nBrowserLoader implements TranslocoLoader {
13 | constructor(private transferState: TransferState, private http: HttpClient) {}
14 |
15 | public getTranslation(language: string): Observable {
16 | const translationsKey: StateKey = makeStateKey(`${TransferStateKey.TRANSLATIONS}-${language}`);
17 | const translationsData: any = this.transferState.get(translationsKey, null);
18 |
19 | if (translationsData) {
20 | return new Observable((observer: Observer) => {
21 | observer.next(translationsData);
22 | observer.complete();
23 | });
24 | }
25 | return this.http.get(`./assets/i18n/${language}.json`);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/core/loaders/i18n-server.loader.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { makeStateKey, StateKey, TransferState } from '@angular/platform-browser';
3 | import { Translation, TranslocoLoader } from '@ngneat/transloco';
4 | import { readFileSync } from 'fs';
5 | import { Observable, Observer } from 'rxjs';
6 |
7 | import { TransferStateKey } from '@core/enums';
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class I18nServerLoader implements TranslocoLoader {
13 | private readonly translationsFolder = 'dist/browser/assets/i18n';
14 |
15 | private readonly translationFileExtension = '.json';
16 |
17 | constructor(private transferState: TransferState) {}
18 |
19 | public getTranslation(language: string): Observable {
20 | return new Observable((observer: Observer) => {
21 | const translationsData: any = JSON.parse(
22 | readFileSync(`${this.translationsFolder}/${language}${this.translationFileExtension}`, 'utf8')
23 | );
24 | const translationsKey: StateKey = makeStateKey(`${TransferStateKey.TRANSLATIONS}-${language}`);
25 | this.transferState.set(translationsKey, translationsData);
26 | observer.next(translationsData);
27 | observer.complete();
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/core/models/error-event.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatusCode } from '@angular/common/http';
2 |
3 | export class ErrorEvent {
4 | public statusCode: HttpStatusCode;
5 |
6 | public statusText: string;
7 |
8 | public message: string;
9 |
10 | public url?: string | null;
11 |
12 | public timestamp?: Date | null;
13 |
14 | constructor(
15 | statusCode: HttpStatusCode,
16 | statusText: string,
17 | message: string,
18 | url?: string | null,
19 | timestamp?: Date | null
20 | ) {
21 | this.statusCode = statusCode;
22 | this.statusText = statusText;
23 | this.message = message;
24 | this.url = url;
25 | this.timestamp = timestamp;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/core/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error-event';
2 |
--------------------------------------------------------------------------------
/src/app/core/resolvers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './language.resolver';
2 |
--------------------------------------------------------------------------------
/src/app/core/resolvers/language.resolver.ts:
--------------------------------------------------------------------------------
1 | import { inject } from '@angular/core';
2 | import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
3 | import { Observable } from 'rxjs';
4 |
5 | import { I18nService } from '@core/services/i18n.service';
6 |
7 | const languageResolver = (
8 | _route: ActivatedRouteSnapshot,
9 | state: RouterStateSnapshot,
10 | i18nService: I18nService = inject(I18nService)
11 | ): string | Observable | Promise => {
12 | const urlLanguage = i18nService.getUrlLanguage(state.url) as string;
13 | const currentLanguage = i18nService.getActiveLanguage();
14 |
15 | if (currentLanguage !== urlLanguage) {
16 | i18nService.initializeLanguage(urlLanguage);
17 | }
18 |
19 | return urlLanguage;
20 | };
21 |
22 | export { languageResolver };
23 |
--------------------------------------------------------------------------------
/src/app/core/services/app-bus.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpErrorResponse } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable, Subject } from 'rxjs';
4 |
5 | import { ErrorEvent } from '@core/models';
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class AppBusService {
11 | private errorSource = new Subject();
12 |
13 | private error$ = this.errorSource.asObservable();
14 |
15 | constructor() {}
16 |
17 | public handleHttpErrors(): Observable {
18 | return this.error$;
19 | }
20 |
21 | public clearHttpErrorsBus() {
22 | this.errorSource.next(null);
23 | }
24 |
25 | public emitHttpError(error: HttpErrorResponse) {
26 | this.errorSource.next(new ErrorEvent(error.status, error.statusText, error.error, error.url, new Date()));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/core/services/app-initializer.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { environment } from '@env/environment';
4 |
5 | import { Logger } from './logger.service';
6 | import { PlatformService } from './platform.service';
7 |
8 | @Injectable({
9 | providedIn: 'root'
10 | })
11 | export class AppInitializer {
12 | constructor(private readonly platformService: PlatformService) {}
13 |
14 | initialize(): Promise {
15 | return new Promise(resolve => {
16 | if (environment.production) {
17 | Logger.enableProductionMode();
18 | }
19 | this.showSelfXssWarningInBrowserConsole();
20 | resolve('App initialized');
21 | });
22 | }
23 |
24 | private showSelfXssWarningInBrowserConsole() {
25 | if (this.platformService.isBrowser) {
26 | console.log('%c🚨 Stop!', 'font-weight:bold; font-size: 4em; color: red; ');
27 | console.log(
28 | `%cThis is a browser feature intended for developers. Using this console may allow attackers to impersonate you and steal your information using an attack called Self-XSS. Do not enter or paste code that you do not understand.`,
29 | 'font-weight:bold; font-size: 1.5em;'
30 | );
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/core/services/app-updater.service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-deprecated */
2 | import { ApplicationRef, Injectable, Optional } from '@angular/core';
3 | import { ActivatedRoute, Data, NavigationEnd, Router } from '@angular/router';
4 | import {
5 | SwUpdate,
6 | VersionDetectedEvent,
7 | VersionEvent,
8 | VersionInstallationFailedEvent,
9 | VersionReadyEvent
10 | } from '@angular/service-worker';
11 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
12 | import { concat, filter, first, interval, map, mergeMap, switchMap, zip } from 'rxjs';
13 |
14 | import { SeoService } from '@app/shared/services';
15 | import { environment } from '@env/environment';
16 |
17 | import { I18nService } from './i18n.service';
18 | import { Logger } from './logger.service';
19 | import { PlatformService } from './platform.service';
20 |
21 | const LOGGER = new Logger('AppUpdater');
22 |
23 | @Injectable({
24 | providedIn: 'root'
25 | })
26 | @UntilDestroy()
27 | export class AppUpdater {
28 | private readonly isBrowserAndServiceWorkerIsEnabled = () =>
29 | this.platformService.isBrowser && this.swUpdate?.isEnabled;
30 |
31 | constructor(
32 | private platformService: PlatformService,
33 | private applicationRef: ApplicationRef,
34 | private readonly router: Router,
35 | private readonly activatedRoute: ActivatedRoute,
36 | private readonly i18nService: I18nService,
37 | private readonly seoService: SeoService,
38 | @Optional() private readonly swUpdate: SwUpdate
39 | ) {
40 | if (this.isBrowserAndServiceWorkerIsEnabled()) {
41 | const appIsStable$ = this.applicationRef.isStable.pipe(first(isStable => isStable === true));
42 | const checkInterval$ = interval(environment.checkForUpdatesInterval * 1000);
43 | const checkIntervalOnceAppIsStable$ = concat(appIsStable$, checkInterval$);
44 | checkIntervalOnceAppIsStable$.pipe(untilDestroyed(this)).subscribe(() => this.swUpdate.checkForUpdate());
45 | }
46 | }
47 |
48 | public handleAppUpdates(): void {
49 | this.handleNewVersionAndUpdateApp();
50 | this.handleRouterChangesAndUpdateMeta();
51 | }
52 |
53 | public handleNewVersionAndUpdateApp(): void {
54 | const eventHandler: Record void> = {
55 | VERSION_DETECTED: (event: VersionDetectedEvent) => {
56 | LOGGER.debug(`Downloading new app version: ${event.version.hash}`);
57 | },
58 | VERSION_READY: (event: VersionReadyEvent) => {
59 | LOGGER.debug(`Current app version: ${event.currentVersion.hash}`);
60 | LOGGER.debug(`New app version ready for use: ${event.latestVersion.hash}`);
61 | this.swUpdate.activateUpdate().then(() => document.location.reload());
62 | },
63 | VERSION_INSTALLATION_FAILED: (event: VersionInstallationFailedEvent) => {
64 | LOGGER.debug(`Failed to install app version '${event.version.hash}': ${event.error}`);
65 | },
66 | DEFAULT: (event: VersionEvent) => {
67 | LOGGER.debug(`Unknown event with type '${event.type}' handled`);
68 | }
69 | };
70 |
71 | if (this.isBrowserAndServiceWorkerIsEnabled()) {
72 | this.swUpdate.versionUpdates.pipe(untilDestroyed(this)).subscribe((event: VersionEvent) => {
73 | (eventHandler[event.type] || eventHandler['DEFAULT'])(event);
74 | });
75 | }
76 | }
77 |
78 | public handleRouterChangesAndUpdateMeta(): void {
79 | this.router.events
80 | .pipe(
81 | filter(event => event instanceof NavigationEnd),
82 | map(() => this.activatedRoute),
83 | map((route: ActivatedRoute) => {
84 | while (route.firstChild) route = route.firstChild;
85 | return route;
86 | }),
87 | filter((route: ActivatedRoute) => route.outlet === 'primary'),
88 | mergeMap((route: ActivatedRoute) => route.data),
89 | switchMap((data: Data) => {
90 | const { titleKey, descriptionKey } = data[`meta`] || {};
91 | const title$ = titleKey
92 | ? this.i18nService.translate(titleKey)
93 | : this.i18nService.translate('routes.default.title');
94 | const description$ = descriptionKey
95 | ? this.i18nService.translate(descriptionKey)
96 | : this.i18nService.translate('routes.default.description');
97 | return zip(title$, description$);
98 | }),
99 | untilDestroyed(this)
100 | )
101 | .subscribe(([title, description]): void => {
102 | this.seoService.setTitle(`${environment.appName} - ${title}`);
103 | this.seoService.setDescription(description);
104 | });
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/app/core/services/cookie.service.ts:
--------------------------------------------------------------------------------
1 | import { DOCUMENT, isPlatformBrowser } from '@angular/common';
2 | import { Inject, Injectable, Optional, PLATFORM_ID } from '@angular/core';
3 | import { REQUEST } from '@nguniversal/express-engine/tokens';
4 | import { Request } from 'express';
5 |
6 | type SameSite = 'Lax' | 'None' | 'Strict';
7 |
8 | interface CookieOptions {
9 | expires?: number | Date;
10 | path?: string;
11 | domain?: string;
12 | secure?: boolean;
13 | sameSite?: SameSite;
14 | }
15 |
16 | @Injectable({
17 | providedIn: 'root'
18 | })
19 | class CookieService {
20 | private readonly documentIsAccessible: boolean;
21 |
22 | constructor(
23 | @Inject(DOCUMENT) private document: Document,
24 | @Inject(PLATFORM_ID) private platformId: string,
25 | @Optional() @Inject(REQUEST) private request: Request
26 | ) {
27 | this.documentIsAccessible = isPlatformBrowser(this.platformId);
28 | }
29 |
30 | /**
31 | * Get cookie Regular Expression
32 | *
33 | * @param name Cookie name
34 | * @returns property RegExp
35 | *
36 | * @author: Stepan Suvorov
37 | * @since: 1.0.0
38 | */
39 | private static getCookieRegExp(name: string): RegExp {
40 | const escapedName: string = name.replace(/([\[\]\{\}\(\)\|\=\;\+\?\,\.\*\^\$])/gi, '\\$1');
41 |
42 | return new RegExp('(?:^' + escapedName + '|;\\s*' + escapedName + ')=(.*?)(?:;|$)', 'g');
43 | }
44 |
45 | /**
46 | * Gets the unencoded version of an encoded component of a Uniform Resource Identifier (URI).
47 | *
48 | * @param encodedURIComponent A value representing an encoded URI component.
49 | *
50 | * @returns The unencoded version of an encoded component of a Uniform Resource Identifier (URI).
51 | *
52 | * @author: Stepan Suvorov
53 | * @since: 1.0.0
54 | */
55 | private static safeDecodeURIComponent(encodedURIComponent: string): string {
56 | try {
57 | return decodeURIComponent(encodedURIComponent);
58 | } catch {
59 | // probably it is not uri encoded. return as is
60 | return encodedURIComponent;
61 | }
62 | }
63 |
64 | /**
65 | * Return `true` if {@link Document} is accessible, otherwise return `false`
66 | *
67 | * @param name Cookie name
68 | * @returns boolean - whether cookie with specified name exists
69 | *
70 | * @author: Stepan Suvorov
71 | * @since: 1.0.0
72 | */
73 | check(name: string): boolean {
74 | name = encodeURIComponent(name);
75 | const regExp: RegExp = CookieService.getCookieRegExp(name);
76 | return regExp.test(this.getPlatformCookies());
77 | }
78 |
79 | /**
80 | * Get cookies by name
81 | *
82 | * @param name Cookie name
83 | * @returns property value
84 | *
85 | * @author: Stepan Suvorov
86 | * @since: 1.0.0
87 | */
88 | get(name: string): string {
89 | if (this.check(name)) {
90 | name = encodeURIComponent(name);
91 |
92 | const regExp: RegExp = CookieService.getCookieRegExp(name);
93 | const result: RegExpExecArray | null = regExp.exec(this.getPlatformCookies());
94 |
95 | return result && result[1] ? CookieService.safeDecodeURIComponent(result[1]) : '';
96 | } else {
97 | return '';
98 | }
99 | }
100 |
101 | /**
102 | * Get all cookies in JSON format
103 | *
104 | * @returns all the cookies in json
105 | *
106 | * @author: Stepan Suvorov
107 | * @since: 1.0.0
108 | */
109 | getAll(): { [key: string]: string } {
110 | const cookies: { [key: string]: string } = {};
111 | const cookiesString: string = this.getPlatformCookies();
112 |
113 | if (cookiesString) {
114 | cookiesString.split(';').forEach((currentCookie: string) => {
115 | const [cookieName, cookieValue] = currentCookie.split('=');
116 | cookies[CookieService.safeDecodeURIComponent(cookieName.replace(/^ /, ''))] =
117 | CookieService.safeDecodeURIComponent(cookieValue);
118 | });
119 | }
120 |
121 | return cookies;
122 | }
123 |
124 | /**
125 | * Set cookie based on provided information
126 | *
127 | * @param name Cookie name
128 | * @param value Cookie value
129 | * @param expires Number of days until the cookies expires or an actual `Date`
130 | * @param path Cookie path
131 | * @param domain Cookie domain
132 | * @param secure Secure flag
133 | * @param sameSite OWASP samesite token `Lax`, `None`, or `Strict`. Defaults to `Lax`
134 | *
135 | * @author: Stepan Suvorov
136 | * @since: 1.0.0
137 | */
138 | set(
139 | name: string,
140 | value: string,
141 | expires?: CookieOptions['expires'],
142 | path?: CookieOptions['path'],
143 | domain?: CookieOptions['domain'],
144 | secure?: CookieOptions['secure'],
145 | sameSite?: SameSite
146 | ): void;
147 |
148 | /**
149 | * Set cookie based on provided information
150 | *
151 | * Cookie's parameters:
152 | *
153 | * expires Number of days until the cookies expires or an actual `Date`
154 | * path Cookie path
155 | * domain Cookie domain
156 | * secure Secure flag
157 | * sameSite OWASP samesite token `Lax`, `None`, or `Strict`. Defaults to `Lax`
158 | *
159 | *
160 | * @param name Cookie name
161 | * @param value Cookie value
162 | * @param options Body with cookie's params
163 | *
164 | * @author: Stepan Suvorov
165 | * @since: 1.0.0
166 | */
167 | set(name: string, value: string, options?: CookieOptions): void;
168 |
169 | set(
170 | name: string,
171 | value: string,
172 | expiresOrOptions?: CookieOptions['expires'] | CookieOptions,
173 | path?: CookieOptions['path'],
174 | domain?: CookieOptions['domain'],
175 | secure?: CookieOptions['secure'],
176 | sameSite?: SameSite
177 | ): void {
178 | if (!this.documentIsAccessible) {
179 | return;
180 | }
181 |
182 | if (
183 | typeof expiresOrOptions === 'number' ||
184 | expiresOrOptions instanceof Date ||
185 | path ||
186 | domain ||
187 | secure ||
188 | sameSite
189 | ) {
190 | const optionsBody = {
191 | expires: expiresOrOptions as CookieOptions['expires'],
192 | path,
193 | domain,
194 | secure,
195 | sameSite: sameSite ? sameSite : 'Lax'
196 | };
197 |
198 | this.set(name, value, optionsBody);
199 | return;
200 | }
201 |
202 | let cookieString: string = encodeURIComponent(name) + '=' + encodeURIComponent(value) + ';';
203 |
204 | const options = expiresOrOptions ? expiresOrOptions : {};
205 |
206 | if (options.expires) {
207 | if (typeof options.expires === 'number') {
208 | const dateExpires: Date = new Date(new Date().getTime() + options.expires * 1000 * 60 * 60 * 24);
209 |
210 | cookieString += 'expires=' + dateExpires.toUTCString() + ';';
211 | } else {
212 | cookieString += 'expires=' + options.expires.toUTCString() + ';';
213 | }
214 | }
215 |
216 | if (options.path) {
217 | cookieString += 'path=' + options.path + ';';
218 | }
219 |
220 | if (options.domain) {
221 | cookieString += 'domain=' + options.domain + ';';
222 | }
223 |
224 | if (options.secure === false && options.sameSite === 'None') {
225 | options.secure = true;
226 | console.warn(
227 | `[ngx-cookie-service] Cookie ${name} was forced with secure flag because sameSite=None.` +
228 | `More details : https://github.com/stevermeister/ngx-cookie-service/issues/86#issuecomment-597720130`
229 | );
230 | }
231 | if (options.secure) {
232 | cookieString += 'secure;';
233 | }
234 |
235 | if (!options.sameSite) {
236 | options.sameSite = 'Lax';
237 | }
238 |
239 | cookieString += 'sameSite=' + options.sameSite + ';';
240 |
241 | // this.document.cookie = cookieString
242 | this.setPlatformCookies(cookieString);
243 | }
244 |
245 | /**
246 | * Delete cookie by name
247 | *
248 | * @param name Cookie name
249 | * @param path Cookie path
250 | * @param domain Cookie domain
251 | * @param secure Cookie secure flag
252 | * @param sameSite Cookie sameSite flag - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
253 | *
254 | * @author: Stepan Suvorov
255 | * @since: 1.0.0
256 | */
257 | delete(
258 | name: string,
259 | path?: CookieOptions['path'],
260 | domain?: CookieOptions['domain'],
261 | secure?: CookieOptions['secure'],
262 | sameSite: SameSite = 'Lax'
263 | ): void {
264 | if (!this.documentIsAccessible) {
265 | return;
266 | }
267 | const expiresDate = new Date('Thu, 01 Jan 1970 00:00:01 GMT');
268 | this.set(name, '', { expires: expiresDate, path, domain, secure, sameSite });
269 | }
270 |
271 | /**
272 | * Delete all cookies
273 | *
274 | * @param path Cookie path
275 | * @param domain Cookie domain
276 | * @param secure Is the Cookie secure
277 | * @param sameSite Is the cookie same site
278 | *
279 | * @author: Stepan Suvorov
280 | * @since: 1.0.0
281 | */
282 | deleteAll(
283 | path?: CookieOptions['path'],
284 | domain?: CookieOptions['domain'],
285 | secure?: CookieOptions['secure'],
286 | sameSite: SameSite = 'Lax'
287 | ): void {
288 | if (!this.documentIsAccessible) {
289 | return;
290 | }
291 |
292 | const cookies: any = this.getAll();
293 |
294 | for (const cookieName in cookies) {
295 | if (cookies.hasOwnProperty(cookieName)) {
296 | this.delete(cookieName, path, domain, secure, sameSite);
297 | }
298 | }
299 | }
300 |
301 | getPlatformCookies(): string {
302 | let cookies = '';
303 | if (this.documentIsAccessible) {
304 | cookies = this.document.cookie;
305 | } else {
306 | cookies = this.request?.headers?.cookie != undefined ? this.request.headers.cookie : '';
307 | }
308 | return cookies;
309 | }
310 |
311 | setPlatformCookies(cookieString: string) {
312 | if (this.documentIsAccessible) {
313 | this.document.cookie = cookieString;
314 | } else {
315 | this.request.headers.cookie = cookieString;
316 | }
317 | }
318 | }
319 |
320 | export { CookieOptions, CookieService, SameSite };
321 |
--------------------------------------------------------------------------------
/src/app/core/services/i18n.service.ts:
--------------------------------------------------------------------------------
1 | import { Location } from '@angular/common';
2 | import { Injectable } from '@angular/core';
3 | import { Router } from '@angular/router';
4 | import { TranslocoService } from '@ngneat/transloco';
5 | import { Observable } from 'rxjs';
6 |
7 | import { Language, LANGUAGES } from '@core/constants';
8 | import { Cookie } from '@core/enums';
9 | import { SeoService, StorageService } from '@shared/services';
10 |
11 | @Injectable({
12 | providedIn: 'root'
13 | })
14 | export class I18nService {
15 | private readonly availableLanguages = LANGUAGES;
16 |
17 | constructor(
18 | private readonly translocoService: TranslocoService,
19 | private readonly router: Router,
20 | private readonly location: Location,
21 | private readonly storageService: StorageService,
22 | private readonly seoService: SeoService
23 | ) {}
24 |
25 | public getAvailableLanguages(): Language[] {
26 | return this.availableLanguages;
27 | }
28 |
29 | public getDefaultLanguage(): string {
30 | return this.translocoService.getDefaultLang();
31 | }
32 |
33 | public getActiveLanguage(): string {
34 | return this.translocoService.getActiveLang();
35 | }
36 |
37 | public getActiveLanguage$(): Observable {
38 | return this.translocoService.langChanges$;
39 | }
40 |
41 | public initializeLanguage(language: string): void {
42 | this.translocoService.setDefaultLang(language);
43 | this.translocoService.setActiveLang(language);
44 | this.seoService.setLanguage(language);
45 | this.saveCookieLanguage(language);
46 | }
47 |
48 | public setActiveLanguage(language: string): void {
49 | this.translocoService.setActiveLang(language);
50 | this.seoService.setLanguage(language);
51 | this.saveCookieLanguage(language);
52 | this.router.navigate([language, ...this.router.url.split('/').slice(2)]);
53 | }
54 |
55 | public translate(key: string): Observable {
56 | return this.translocoService.selectTranslate(key);
57 | }
58 |
59 | public getUrlLanguage(url?: string): string | null {
60 | const pathSlices = (url || this.location.path() || '').split('#')[0].split('?')[0].split('/');
61 | if (pathSlices.length > 1 && this.availableLanguages.some(language => language.id === pathSlices[1])) {
62 | return pathSlices[1];
63 | }
64 | if (pathSlices.length && this.availableLanguages.some(language => language.id === pathSlices[0])) {
65 | return pathSlices[0];
66 | }
67 | return null;
68 | }
69 |
70 | public getCookieLanguage(): string | undefined {
71 | return this.storageService.getCookie(Cookie.LANGUAGE);
72 | }
73 |
74 | public saveCookieLanguage(language: string): void {
75 | this.storageService.saveCookie(Cookie.LANGUAGE, language, 7);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/core/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './app-bus.service';
2 | export * from './app-initializer.service';
3 | export * from './app-updater.service';
4 | export * from './i18n.service';
5 | export * from './logger.service';
6 | export * from './platform.service';
7 |
--------------------------------------------------------------------------------
/src/app/core/services/logger.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Logger, LogLevel, LogOutput } from './logger.service';
2 |
3 | const logMethods = ['log', 'info', 'warn', 'error'];
4 |
5 | describe('Core -> Logger', () => {
6 | let savedConsole: any[];
7 | let savedLevel: LogLevel;
8 | let savedOutputs: LogOutput[];
9 |
10 | beforeAll(() => {
11 | savedConsole = [];
12 | logMethods.forEach((m: any) => {
13 | savedConsole[m] = console[m as keyof Console];
14 | console[m as keyof Console] = jest.fn();
15 | });
16 | savedLevel = Logger.level;
17 | savedOutputs = Logger.outputs;
18 | });
19 |
20 | beforeEach(() => {
21 | Logger.level = LogLevel.DEBUG;
22 | });
23 |
24 | afterAll(() => {
25 | logMethods.forEach((m: any) => {
26 | console[m as keyof Console] = savedConsole[m];
27 | });
28 | Logger.level = savedLevel;
29 | Logger.outputs = savedOutputs;
30 | });
31 |
32 | it('should create an instance', () => {
33 | expect(new Logger()).toBeTruthy();
34 | });
35 |
36 | it('should add a new LogOutput and receives log entries', () => {
37 | const outputSpy = jest.fn();
38 | const log = new Logger('test');
39 |
40 | Logger.outputs.push(outputSpy);
41 |
42 | log.debug('d');
43 | log.info('i');
44 | log.warn('w');
45 | log.error('e', { error: true });
46 |
47 | expect(outputSpy).toHaveBeenCalled();
48 | expect(outputSpy.mock.calls).toHaveLength(4);
49 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.DEBUG, 'd');
50 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.INFO, 'i');
51 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.WARNING, 'w');
52 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.ERROR, 'e', { error: true });
53 | });
54 |
55 | it('should add a new LogOutput and receives only production log entries', () => {
56 | // Arrange
57 | const outputSpy = jest.fn();
58 | const log = new Logger('test');
59 |
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 | expect(outputSpy).toHaveBeenCalled();
69 | expect(outputSpy.mock.calls).toHaveLength(2);
70 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.WARNING, 'w');
71 | expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.ERROR, 'e', { error: true });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/src/app/core/services/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 '@core/services/logger.service';
13 | *
14 | * const LOGGER = new Logger('myFile');
15 | * ...
16 | * logger.debug('something happened');
17 | * ```
18 | *
19 | * If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs.
20 | */
21 |
22 | enum LogLevel {
23 | OFF = 0,
24 | ERROR = 1,
25 | WARNING = 2,
26 | INFO = 3,
27 | DEBUG = 4
28 | }
29 |
30 | type LogOutput = (source: string | undefined, level: LogLevel, ...objects: any[]) => void;
31 |
32 | class Logger {
33 | static level = LogLevel.DEBUG;
34 |
35 | static outputs: LogOutput[] = [];
36 |
37 | constructor(private source?: string) {}
38 |
39 | static enableProductionMode() {
40 | Logger.level = LogLevel.WARNING;
41 | }
42 |
43 | debug(...objects: any[]) {
44 | this.log(console.log, LogLevel.DEBUG, objects);
45 | }
46 |
47 | info(...objects: any[]) {
48 | this.log(console.info, LogLevel.INFO, objects);
49 | }
50 |
51 | warn(...objects: any[]) {
52 | this.log(console.warn, LogLevel.WARNING, objects);
53 | }
54 |
55 | error(...objects: any[]) {
56 | this.log(console.error, LogLevel.ERROR, objects);
57 | }
58 |
59 | private log(func: (...args: any[]) => void, level: LogLevel, objects: any[]) {
60 | if (level <= Logger.level) {
61 | const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects;
62 | func.apply(console, log);
63 | Logger.outputs.forEach(output => output.apply(output, [this.source, level, ...objects]));
64 | }
65 | }
66 | }
67 |
68 | export { Logger, LogLevel, LogOutput };
69 |
--------------------------------------------------------------------------------
/src/app/core/services/platform.service.ts:
--------------------------------------------------------------------------------
1 | import { isPlatformBrowser, isPlatformServer } from '@angular/common';
2 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
3 |
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class PlatformService {
8 | public readonly isBrowser = isPlatformBrowser(this.platformId);
9 |
10 | public readonly isServer = isPlatformServer(this.platformId);
11 |
12 | constructor(@Inject(PLATFORM_ID) private platformId: any) {}
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/core/strategies/custom-lazyload-image.strategy.ts:
--------------------------------------------------------------------------------
1 | import { isPlatformServer } from '@angular/common';
2 | import { IntersectionObserverHooks } from 'ng-lazyload-image';
3 |
4 | export class CustomLazyLoadImageStrategy extends IntersectionObserverHooks {
5 | override isBot() {
6 | return isPlatformServer(this.platformId);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/core/strategies/custom-page-title.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Title } from '@angular/platform-browser';
3 | import { RouterStateSnapshot, TitleStrategy } from '@angular/router';
4 |
5 | import { environment } from '@env/environment';
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class CustomPageTitleStrategy extends TitleStrategy {
11 | constructor(private readonly title: Title) {
12 | super();
13 | }
14 |
15 | override updateTitle(routerState: RouterStateSnapshot) {
16 | const title = this.buildTitle(routerState);
17 | if (title !== undefined) {
18 | this.title.setTitle(`${environment.appName} - ${title}`);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/core/strategies/custom-route-preload.strategy.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-deprecated */
2 | import { ApplicationRef, Injectable } from '@angular/core';
3 | import { PreloadingStrategy, Route } from '@angular/router';
4 | import { concat, EMPTY, first, Observable, switchMap, timer, toArray } from 'rxjs';
5 |
6 | import { PlatformService } from '@core/services';
7 |
8 | @Injectable({
9 | providedIn: 'root'
10 | })
11 | export class CustomRoutePreloadingStrategy implements PreloadingStrategy {
12 | private preloadedModules = new Set();
13 |
14 | constructor(private readonly platformService: PlatformService, private applicationRef: ApplicationRef) {}
15 |
16 | preload(route: Route, load: () => Observable): Observable {
17 | const loadRoute = (delay: number) => {
18 | this.preloadedModules.add(route);
19 | const appIsStable$ = this.applicationRef.isStable.pipe(first(isStable => isStable === true));
20 | const preloadTimer$ = timer(delay * 1000);
21 | const preloadRouteOnceAppIsStable$ = concat(appIsStable$, preloadTimer$).pipe(
22 | toArray(),
23 | switchMap(() => load())
24 | );
25 | return delay > 0 ? preloadRouteOnceAppIsStable$ : load();
26 | };
27 | return this.shouldPreloadRoute(route) ? loadRoute(this.getDelay(route)) : EMPTY;
28 | }
29 |
30 | private shouldPreloadRoute(route: Route): boolean {
31 | if (this.platformService.isServer) {
32 | return false;
33 | }
34 |
35 | return !this.preloadedModules.has(route) && route.data && route.data['preload'];
36 | }
37 |
38 | private getDelay(route: Route): number {
39 | return route.data && route.data['delayInSeconds'] ? route.data['delayInSeconds'] : 0;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/core/strategies/custom-route-reuse.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { ActivatedRouteSnapshot, DetachedRouteHandle, Route, RouteReuseStrategy } from '@angular/router';
3 |
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class CustomRouteReuseStrategy implements RouteReuseStrategy {
8 | private readonly detachedRouteHandles = new WeakMap();
9 |
10 | shouldDetach(route: ActivatedRouteSnapshot): boolean {
11 | return this.shouldBeReused(route);
12 | }
13 |
14 | public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
15 | handle
16 | ? this.detachedRouteHandles.set(route.routeConfig!, handle)
17 | : this.detachedRouteHandles.delete(route.routeConfig!);
18 | }
19 |
20 | shouldAttach(route: ActivatedRouteSnapshot): boolean {
21 | return this.shouldBeReused(route) ? this.detachedRouteHandles.has(route.routeConfig!) : false;
22 | }
23 |
24 | retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
25 | return this.shouldBeReused(route) ? this.detachedRouteHandles.get(route.routeConfig!)! : null;
26 | }
27 |
28 | public shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
29 | return future.routeConfig === current.routeConfig || this.shouldBeReused(future);
30 | }
31 |
32 | private shouldBeReused(route: ActivatedRouteSnapshot): boolean {
33 | return !!route.data['shouldReuse'];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/core/strategies/index.ts:
--------------------------------------------------------------------------------
1 | export * from './custom-page-title.strategy';
2 | export * from './custom-route-preload.strategy';
3 | export * from './custom-route-reuse.strategy';
4 | export * from './network-aware-route-preload.strategy';
5 |
--------------------------------------------------------------------------------
/src/app/core/strategies/network-aware-route-preload.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { PreloadingStrategy, Route } from '@angular/router';
3 | import { EMPTY, Observable } from 'rxjs';
4 |
5 | import { PlatformService } from '@core/services';
6 |
7 | const SLOW_CONNECTIONS = ['slow-2g', '2g', '3g'];
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class NetworkAwareRoutePreloadingStrategy implements PreloadingStrategy {
13 | private preloadedModules = new Set();
14 |
15 | constructor(private readonly platformService: PlatformService) {}
16 |
17 | preload(route: Route, load: () => Observable): Observable {
18 | const loadRoute = () => {
19 | this.preloadedModules.add(route);
20 | return load();
21 | };
22 | return this.shouldPreloadRoute(route) ? loadRoute() : EMPTY;
23 | }
24 |
25 | private shouldPreloadRoute(route: Route): boolean {
26 | if (this.platformService.isServer) {
27 | return false;
28 | }
29 |
30 | const connection = (navigator as any)?.connection;
31 | const currentSpeed = connection.effectiveType;
32 |
33 | return !this.preloadedModules.has(route) && !connection.saveData && !SLOW_CONNECTIONS.includes(currentSpeed!);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/core/tokens/app-name.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 |
3 | export const APP_NAME = new InjectionToken('App Name');
4 |
--------------------------------------------------------------------------------
/src/app/core/utils/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/core/utils/index.ts
--------------------------------------------------------------------------------
/src/app/features/home/components/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/features/home/components/index.ts
--------------------------------------------------------------------------------
/src/app/features/home/home-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { WelcomeComponent } from './pages';
5 |
6 | const routes: Routes = [{ path: '', component: WelcomeComponent }];
7 |
8 | @NgModule({
9 | imports: [RouterModule.forChild(routes)],
10 | exports: [RouterModule]
11 | })
12 | export class HomeRoutingModule {}
13 |
--------------------------------------------------------------------------------
/src/app/features/home/home.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { SharedModule } from '@shared/shared.module';
5 |
6 | import { HomeRoutingModule } from './home-routing.module';
7 | import { WelcomeComponent } from './pages';
8 |
9 | @NgModule({
10 | declarations: [WelcomeComponent],
11 | imports: [CommonModule, HomeRoutingModule, SharedModule]
12 | })
13 | export class HomeModule {}
14 |
--------------------------------------------------------------------------------
/src/app/features/home/models/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/features/home/models/index.ts
--------------------------------------------------------------------------------
/src/app/features/home/pages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './welcome/welcome.component';
2 |
--------------------------------------------------------------------------------
/src/app/features/home/pages/welcome/welcome.component.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/app/features/home/pages/welcome/welcome.component.scss:
--------------------------------------------------------------------------------
1 | :host div {
2 | @apply flex h-full items-center justify-center;
3 |
4 | p {
5 | @apply mb-7 border-2 border-dashed border-stone-600 px-3 py-2.5 font-bold border-sketch-2 dark:border-stone-100;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/features/home/pages/welcome/welcome.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-welcome',
5 | templateUrl: './welcome.component.html',
6 | styleUrls: ['./welcome.component.scss']
7 | })
8 | export class WelcomeComponent {
9 | constructor() {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/features/home/services/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/features/home/services/index.ts
--------------------------------------------------------------------------------
/src/app/features/jokes/components/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/features/jokes/components/index.ts
--------------------------------------------------------------------------------
/src/app/features/jokes/jokes-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { ChuckNorrisComponent } from './pages';
5 |
6 | const routes: Routes = [
7 | { path: '', redirectTo: 'chuck-norris', pathMatch: 'full' },
8 | { path: 'chuck-norris', component: ChuckNorrisComponent, data: { shouldReuse: true, revalidate: 5 } }
9 | ];
10 |
11 | @NgModule({
12 | imports: [RouterModule.forChild(routes)],
13 | exports: [RouterModule]
14 | })
15 | export class JokesRoutingModule {}
16 |
--------------------------------------------------------------------------------
/src/app/features/jokes/jokes.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { SharedModule } from '@app/shared/shared.module';
5 |
6 | import { JokesRoutingModule } from './jokes-routing.module';
7 | import { ChuckNorrisComponent } from './pages';
8 | import { ChuckNorrisService } from './services';
9 |
10 | @NgModule({
11 | declarations: [ChuckNorrisComponent],
12 | imports: [CommonModule, JokesRoutingModule, SharedModule],
13 | providers: [ChuckNorrisService]
14 | })
15 | export class JokesModule {}
16 |
--------------------------------------------------------------------------------
/src/app/features/jokes/models/chuck-norris-joke.model.ts:
--------------------------------------------------------------------------------
1 | export interface IChuckNorrisJoke {
2 | category: string;
3 | icon_url: string;
4 | id: string;
5 | url: string;
6 | value: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/features/jokes/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chuck-norris-joke.model';
2 |
--------------------------------------------------------------------------------
/src/app/features/jokes/pages/chuck-norris/chuck-norris.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ randomJoke.value }}
5 |
6 |
{{ t('getJoke') }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/app/features/jokes/pages/chuck-norris/chuck-norris.component.scss:
--------------------------------------------------------------------------------
1 | :host div {
2 | @apply mx-auto flex h-full max-w-lg flex-wrap content-center items-center justify-center overflow-hidden text-center align-middle;
3 |
4 | p {
5 | @apply mb-7 border-2 border-dashed border-stone-600 px-3 py-2.5 font-bold border-sketch-2 dark:border-stone-100;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/features/jokes/pages/chuck-norris/chuck-norris.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
3 | import { Observable, ReplaySubject } from 'rxjs';
4 |
5 | import { IChuckNorrisJoke } from '@features/jokes/models';
6 | import { ChuckNorrisService } from '@features/jokes/services';
7 |
8 | @Component({
9 | selector: 'app-chuck-norris',
10 | templateUrl: './chuck-norris.component.html',
11 | styleUrls: ['./chuck-norris.component.scss']
12 | })
13 | @UntilDestroy()
14 | export class ChuckNorrisComponent implements OnInit {
15 | private _randomJoke = new ReplaySubject(1);
16 |
17 | public randomJoke$: Observable = this._randomJoke.asObservable();
18 |
19 | constructor(private chuckNorrisService: ChuckNorrisService) {}
20 |
21 | ngOnInit() {
22 | this.getRandomJoke();
23 | }
24 |
25 | public getRandomJoke() {
26 | this.chuckNorrisService
27 | .getRandomJoke()
28 | .pipe(untilDestroyed(this))
29 | .subscribe(joke => this._randomJoke.next(joke));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/features/jokes/pages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chuck-norris/chuck-norris.component';
2 |
--------------------------------------------------------------------------------
/src/app/features/jokes/services/chuck-norris.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 |
5 | import { IChuckNorrisJoke } from '@features/jokes/models';
6 |
7 | @Injectable()
8 | export class ChuckNorrisService {
9 | private readonly randomJokeUrl = 'https://api.chucknorris.io/jokes/random';
10 |
11 | constructor(private http: HttpClient) {}
12 |
13 | public getRandomJoke(): Observable {
14 | return this.http.get(this.randomJokeUrl);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/features/jokes/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chuck-norris.service';
2 |
--------------------------------------------------------------------------------
/src/app/shared/components/error/error.component.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ errorData$ | async | json }}
3 |
4 |
--------------------------------------------------------------------------------
/src/app/shared/components/error/error.component.scss:
--------------------------------------------------------------------------------
1 | :host div {
2 | @apply flex h-full items-center justify-center;
3 |
4 | p {
5 | @apply mb-7 border-2 border-dashed border-stone-600 px-3 py-2.5 font-bold border-sketch-2 dark:border-stone-100;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/shared/components/error/error.component.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-deprecated */
2 | import { HttpStatusCode } from '@angular/common/http';
3 | import { Component, Input, OnInit } from '@angular/core';
4 | import { combineLatest, map, Observable } from 'rxjs';
5 |
6 | import { I18nService } from '@core/services';
7 | import { ServerService } from '@shared/services';
8 |
9 | const ALLOWED_ERROR_CODES = [HttpStatusCode.NotFound, HttpStatusCode.GatewayTimeout];
10 | const DEFAULT_ERROR_CODE = HttpStatusCode.NotFound;
11 |
12 | @Component({
13 | selector: 'app-error',
14 | templateUrl: './error.component.html',
15 | styleUrls: ['./error.component.scss']
16 | })
17 | export class ErrorComponent implements OnInit {
18 | @Input() public errorCode = DEFAULT_ERROR_CODE;
19 |
20 | public readonly errorData$: Observable<{ code: number; title: string; description: string }>;
21 |
22 | constructor(private readonly i18nService: I18nService, private readonly serverService: ServerService) {
23 | const isValidErrorCode = ALLOWED_ERROR_CODES.includes(this.errorCode);
24 | const titleKey = isValidErrorCode ? `errors.${this.errorCode}.title` : 'errors.unknown.title';
25 | const descriptionKey = isValidErrorCode ? `errors.${this.errorCode}.description` : 'errors.unknown.description';
26 | this.errorData$ = combineLatest([
27 | this.i18nService.translate(titleKey),
28 | this.i18nService.translate(descriptionKey)
29 | ]).pipe(map(([title, description]) => ({ code: this.errorCode, title, description })));
30 | }
31 |
32 | ngOnInit(): void {
33 | const errorHandler: Record void> = {
34 | [HttpStatusCode.NotFound]: () => this.serverService.setNotFoundResponse(),
35 | 0: () => this.serverService.setInternalServerErrorResponse()
36 | };
37 | (errorHandler[this.errorCode] || errorHandler[0])();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/shared/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error/error.component';
2 |
--------------------------------------------------------------------------------
/src/app/shared/directives/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/shared/directives/index.ts
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/github-icon/github-icon.component.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/github-icon/github-icon.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/shared/modules/icons/github-icon/github-icon.component.scss
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/github-icon/github-icon.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-github-icon',
5 | templateUrl: './github-icon.component.html',
6 | styleUrls: ['./github-icon.component.scss']
7 | })
8 | export class GithubIconComponent {
9 | constructor() {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/happy-icon/happy-icon.component.html:
--------------------------------------------------------------------------------
1 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/happy-icon/happy-icon.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/shared/modules/icons/happy-icon/happy-icon.component.scss
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/happy-icon/happy-icon.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-happy-icon',
5 | templateUrl: './happy-icon.component.html',
6 | styleUrls: ['./happy-icon.component.scss']
7 | })
8 | export class HappyIconComponent {
9 | constructor() {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/heart-icon/heart-icon.component.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/heart-icon/heart-icon.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/app/shared/modules/icons/heart-icon/heart-icon.component.scss
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/heart-icon/heart-icon.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-heart-icon',
5 | templateUrl: './heart-icon.component.html',
6 | styleUrls: ['./heart-icon.component.scss']
7 | })
8 | export class HeartIconComponent {
9 | constructor() {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/shared/modules/icons/icons.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, Type } from '@angular/core';
2 |
3 | import { GithubIconComponent } from './github-icon/github-icon.component';
4 | import { HappyIconComponent } from './happy-icon/happy-icon.component';
5 | import { HeartIconComponent } from './heart-icon/heart-icon.component';
6 |
7 | const ICONS: Type[] = [HappyIconComponent, HeartIconComponent, GithubIconComponent];
8 |
9 | @NgModule({
10 | declarations: [...ICONS],
11 | imports: [],
12 | exports: [...ICONS]
13 | })
14 | export class IconsModule {}
15 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/index.ts:
--------------------------------------------------------------------------------
1 | export * from './localize-route.pipe';
2 |
--------------------------------------------------------------------------------
/src/app/shared/pipes/localize-route.pipe.ts:
--------------------------------------------------------------------------------
1 | import { OnDestroy, Pipe, PipeTransform } from '@angular/core';
2 | import * as equal from 'fast-deep-equal';
3 | import { Subscription } from 'rxjs';
4 |
5 | import { I18nService } from '@core/services';
6 |
7 | @Pipe({
8 | name: 'localizeRoute',
9 | // Required to update the value when the promise is resolved
10 | pure: false
11 | })
12 | export class LocalizeRoutePipe implements PipeTransform, OnDestroy {
13 | private value: string | any[] = '';
14 |
15 | private lastRoute: string | any[] = '';
16 |
17 | private lastLanguage: string = this.i18nService.getActiveLanguage();
18 |
19 | private languageChanges: Subscription = this.i18nService.getActiveLanguage$().subscribe(() => {
20 | this.transform(this.lastRoute);
21 | });
22 |
23 | constructor(private readonly i18nService: I18nService) {}
24 |
25 | ngOnDestroy() {
26 | if (this.languageChanges) {
27 | this.languageChanges.unsubscribe();
28 | }
29 | }
30 |
31 | transform(route: string | any[]): string | any[] {
32 | if (!route || route.length === 0 || !this.i18nService.getActiveLanguage()) {
33 | return route;
34 | }
35 |
36 | if (equal(route, this.lastRoute) && equal(this.lastLanguage, this.i18nService.getActiveLanguage())) {
37 | return this.value;
38 | }
39 |
40 | const activeLanguage = this.i18nService.getActiveLanguage();
41 | this.lastLanguage = this.i18nService.getActiveLanguage();
42 | this.lastRoute = route;
43 |
44 | this.value = this.getLocalizedRoute(route, activeLanguage);
45 |
46 | return this.value;
47 | }
48 |
49 | private getLocalizedRoute(path: string | any[], language: string): string | any[] {
50 | if (typeof path === 'string') {
51 | return path.indexOf('/') == 0 ? `/${language}${path}` : path;
52 | }
53 |
54 | let result: any[] = [];
55 | path.forEach((segment: any, index: number) => {
56 | if (typeof segment === 'string') {
57 | const isRootRoute = index == 0 && segment.indexOf('/') == 0;
58 | result.push(isRootRoute ? `/${language}${segment}` : segment);
59 | }
60 | });
61 |
62 | return result;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/app/shared/services/browser.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { PlatformService } from '@core/services';
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class BrowserService {
9 | constructor(private readonly platformService: PlatformService) {}
10 |
11 | public getBrowserLanguage(): string | undefined {
12 | return this.platformService.isBrowser ? navigator.language.substring(0, 2) : undefined;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/shared/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './browser.service';
2 | export * from './seo.service';
3 | export * from './server.service';
4 | export * from './storage.service';
5 |
--------------------------------------------------------------------------------
/src/app/shared/services/seo.service.ts:
--------------------------------------------------------------------------------
1 | import { DOCUMENT } from '@angular/common';
2 | import { Inject, Injectable } from '@angular/core';
3 | import { Meta, Title } from '@angular/platform-browser';
4 |
5 | import { environment } from '@env/environment';
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class SeoService {
11 | constructor(private title: Title, private meta: Meta, @Inject(DOCUMENT) private readonly document: Document) {}
12 |
13 | public setLanguage(language: string) {
14 | this.document.documentElement.lang = language;
15 | }
16 |
17 | public setTitle(title: string) {
18 | this.title.setTitle(title);
19 | this.meta.updateTag({ itemprop: 'name', content: title }, 'itemprop="name"');
20 | this.meta.updateTag({ name: 'twitter:title', content: title });
21 | this.meta.updateTag({ property: 'og:title', content: title });
22 | }
23 |
24 | public setDescription(description: string) {
25 | this.meta.updateTag({ name: 'description', content: description });
26 | this.meta.updateTag({ itemprop: 'description', content: description }, 'itemprop="description"');
27 | this.meta.updateTag({ name: 'twitter:description', content: description });
28 | this.meta.updateTag({ property: 'og:description', content: description });
29 | }
30 |
31 | public setUrl(path: string) {
32 | this.meta.updateTag({ property: 'og:url', content: environment.baseUrl + path });
33 | }
34 |
35 | public setImage(url: string) {
36 | this.meta.updateTag({ itemprop: 'image', content: url }, 'itemprop="image"');
37 | this.meta.updateTag({ name: 'twitter:image', content: url });
38 | this.meta.updateTag({ name: 'twitter:image:src', content: url });
39 | this.meta.updateTag({ property: 'og:image', content: url });
40 | this.meta.updateTag({ property: 'og:image:secure_url', content: url });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/shared/services/server.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Optional } from '@angular/core';
2 | import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
3 | import { Request, Response } from 'express';
4 | import { getReasonPhrase, StatusCodes } from 'http-status-codes';
5 | import isEmpty from 'just-is-empty';
6 |
7 | import { Header } from '@core/enums';
8 | import { PlatformService } from '@core/services/platform.service';
9 |
10 | @Injectable({
11 | providedIn: 'root'
12 | })
13 | export class ServerService {
14 | private readonly isServerAndResponseIsNotEmpty = () => this.platformService.isServer && !isEmpty(this.response);
15 |
16 | private readonly isServerAndRequestIsNotEmpty = () => this.platformService.isServer && !isEmpty(this.request);
17 |
18 | constructor(
19 | private readonly platformService: PlatformService,
20 | @Optional() @Inject(REQUEST) private readonly request: Request,
21 | @Optional() @Inject(RESPONSE) private readonly response: Response
22 | ) {}
23 |
24 | public setResponseStatus(code: StatusCodes, message?: string, headers?: { [header: string]: string | number }) {
25 | if (this.isServerAndResponseIsNotEmpty()) {
26 | this.response.status(code);
27 |
28 | if (!isEmpty(message)) {
29 | (this.response.statusMessage as any) = message;
30 | }
31 |
32 | if (!isEmpty(headers)) {
33 | for (const header in headers) {
34 | this.response.setHeader(header, headers[header]);
35 | }
36 | }
37 | }
38 | }
39 |
40 | public setRedirectResponse(url: string, permanently: boolean = true) {
41 | const redirectStatusCode = permanently ? StatusCodes.MOVED_PERMANENTLY : StatusCodes.MOVED_TEMPORARILY;
42 | this.setResponseStatus(redirectStatusCode, getReasonPhrase(redirectStatusCode));
43 | this.setHeader('Location', url);
44 | }
45 |
46 | public setNotFoundResponse(message = getReasonPhrase(StatusCodes.NOT_FOUND)) {
47 | this.setResponseStatus(StatusCodes.NOT_FOUND, message);
48 | }
49 |
50 | public setInternalServerErrorResponse(message = getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR)) {
51 | this.setResponseStatus(StatusCodes.INTERNAL_SERVER_ERROR, message);
52 | }
53 |
54 | public getLanguageHeader(): string | undefined {
55 | return this.getHeader(Header.ACCEPT_LANGUAGE)?.substring(0, 2);
56 | }
57 |
58 | public getHeader(header: string): string | undefined {
59 | if (this.isServerAndRequestIsNotEmpty()) {
60 | return (this.request.headers as any)[header];
61 | }
62 | return undefined;
63 | }
64 |
65 | public setHeader(header: string, value: string | number) {
66 | if (this.isServerAndResponseIsNotEmpty()) {
67 | this.response.setHeader(header, value);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/app/shared/services/storage.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { CookieService } from '@app/core/services/cookie.service';
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class StorageService {
9 | constructor(private readonly cookieService: CookieService) {}
10 |
11 | public saveCookie(key: string, value: string, validityTimeInDays: number): void {
12 | const expirestAt = new Date();
13 | expirestAt.setDate(expirestAt.getDate() + validityTimeInDays);
14 | this.cookieService.set(key, value, { expires: expirestAt, path: '/' });
15 | }
16 |
17 | public getCookie(key: string): string | undefined {
18 | return this.cookieService.get(key);
19 | }
20 |
21 | public removeCookie(key: string): void {
22 | this.cookieService.delete(key);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { ModuleWithProviders, NgModule, Type } from '@angular/core';
3 | import { TranslocoModule } from '@ngneat/transloco';
4 | import { LazyLoadImageModule } from 'ng-lazyload-image';
5 |
6 | import { ErrorComponent } from './components';
7 | import { IconsModule } from './modules/icons/icons.module';
8 | import { LocalizeRoutePipe } from './pipes';
9 |
10 | const SHARED_ITEMS: Type[] = [LocalizeRoutePipe, ErrorComponent];
11 |
12 | @NgModule({
13 | declarations: [...SHARED_ITEMS],
14 | imports: [CommonModule],
15 | exports: [TranslocoModule, IconsModule, LazyLoadImageModule, ...SHARED_ITEMS]
16 | })
17 | export class SharedModule {
18 | constructor() {}
19 |
20 | static forRoot(): ModuleWithProviders {
21 | return { ngModule: SharedModule, providers: [] };
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n": {
3 | "es": "Spanish",
4 | "en": "English"
5 | },
6 | "navbar": {
7 | "welcome": "Welcome",
8 | "jokes": "Chuck Norris Jokes",
9 | "notFound": "Not Found"
10 | },
11 | "routes": {
12 | "default": {
13 | "title": "Angular Skeleton",
14 | "description": "Angular Skeleton"
15 | },
16 | "welcome": {
17 | "title": "Welcome",
18 | "description": "Welcome page"
19 | },
20 | "jokes": {
21 | "title": "Chuck Norris jokes",
22 | "description": "Chuck Norris jokes page"
23 | }
24 | },
25 | "features": {
26 | "jokes": {
27 | "getJoke": "Get random joke"
28 | }
29 | },
30 | "errors": {
31 | "404": {
32 | "title": "Not Found",
33 | "description": "The page you are looking for does not exist"
34 | },
35 | "504": {
36 | "title": "Network Error",
37 | "description": "Please, check your internet connection and try again"
38 | },
39 | "unknown": {
40 | "title": "Unknown error",
41 | "description": "Unknown error"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/i18n/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n": {
3 | "es": "Español",
4 | "en": "Inglés"
5 | },
6 | "navbar": {
7 | "welcome": "Bienvenidx",
8 | "jokes": "Bromas de Chuck Norris",
9 | "notFound": "Not Found"
10 | },
11 | "routes": {
12 | "default": {
13 | "title": "Angular Skeleton",
14 | "description": "Angular Skeleton"
15 | },
16 | "welcome": {
17 | "title": "Bienvenido",
18 | "description": "Página de bienvenida"
19 | },
20 | "jokes": {
21 | "title": "Bromas de Chuck Norris",
22 | "description": "Bromas de Chuck Norris"
23 | }
24 | },
25 | "features": {
26 | "jokes": {
27 | "getJoke": "Obtener chiste aleatorio"
28 | }
29 | },
30 | "errors": {
31 | "404": {
32 | "title": "Not Found",
33 | "description": "La página que buscas no existe"
34 | },
35 | "504": {
36 | "title": "Network Error",
37 | "description": "Por favor, comprueba tu conexión a internet y vuelve a intentarlo"
38 | },
39 | "unknown": {
40 | "title": "Unknown Error",
41 | "description": "Error desconocido"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-128x128.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-152x152.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-192x192.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-384x384.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-512x512.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-72x72.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/icons/icon-96x96.png
--------------------------------------------------------------------------------
/src/assets/images/background.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-restricted-imports
2 | import packageJson from '../../package.json';
3 |
4 | export const environment = {
5 | production: true,
6 | appName: 'Angular Skeleton',
7 | appVersion: packageJson.version,
8 | appDescription: 'Angular Skeleton',
9 | author: '@borjapazr',
10 | authorEmail: 'borjapazr@gmail.com',
11 | authorWebsite: 'https://bpaz.dev',
12 | defaultLanguage: 'en',
13 | darkModeAsDefault: false,
14 | checkForUpdatesInterval: 60,
15 | baseUrl: 'http://localhost:4200'
16 | };
17 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | // eslint-disable-next-line no-restricted-imports
6 | import packageJson from '../../package.json';
7 |
8 | export const environment = {
9 | production: false,
10 | appName: 'Angular Skeleton',
11 | appVersion: packageJson.version,
12 | appDescription: 'Angular Skeleton',
13 | author: '@borjapazr',
14 | authorEmail: 'borjapazr@gmail.com',
15 | authorWebsite: 'https://bpaz.dev',
16 | defaultLanguage: 'en',
17 | darkModeAsDefault: false,
18 | checkForUpdatesInterval: 60,
19 | baseUrl: 'http://localhost:4200'
20 | };
21 |
22 | /*
23 | * For easier debugging in development mode, you can import the following file
24 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
25 | *
26 | * This import should be commented out in production mode because it will have a negative impact
27 | * on performance if an error is thrown.
28 | */
29 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
30 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/favicon.ico
--------------------------------------------------------------------------------
/src/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/borjapazr/angular-skeleton/eb7bd14e9b83440930634b34d53058f8d5e98c3e/src/favicon.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Angular Skeleton
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
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 |
63 |
64 |
65 |
66 | Please enable JavaScript to continue using this application.
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/jest.mocks.ts:
--------------------------------------------------------------------------------
1 | import { jest } from '@jest/globals';
2 |
3 | Object.defineProperty(window, 'CSS', { value: null });
4 |
5 | Object.defineProperty(document, 'doctype', {
6 | value: ''
7 | });
8 |
9 | Object.defineProperty(window, 'getComputedStyle', {
10 | value: () => {
11 | return {
12 | display: 'none',
13 | appearance: ['-webkit-appearance']
14 | };
15 | }
16 | });
17 |
18 | /**
19 | * ISSUE: https://github.com/angular/material2/issues/7101
20 | * Workaround for JSDOM missing transform property
21 | */
22 | Object.defineProperty(document.body.style, 'transform', {
23 | value: () => {
24 | return {
25 | enumerable: true,
26 | configurable: true
27 | };
28 | }
29 | });
30 |
31 | HTMLCanvasElement.prototype.getContext = jest.fn();
32 |
--------------------------------------------------------------------------------
/src/jest.setup.ts:
--------------------------------------------------------------------------------
1 | import 'jest-preset-angular/setup-jest';
2 | import './jest.mocks';
3 |
--------------------------------------------------------------------------------
/src/main.browser.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppBrowserModule } from './app/app.browser.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | const bootstrap = () => {
12 | platformBrowserDynamic()
13 | .bootstrapModule(AppBrowserModule)
14 | .catch(err => console.error(err));
15 | };
16 |
17 | if (document.readyState === 'complete') {
18 | bootstrap();
19 | } else {
20 | document.addEventListener('DOMContentLoaded', bootstrap);
21 | }
22 |
--------------------------------------------------------------------------------
/src/main.server.ts:
--------------------------------------------------------------------------------
1 | /***************************************************************************************************
2 | * Initialize the server environment - for example, adding DOM built-in types to the global scope.
3 | *
4 | * NOTE:
5 | * This import must come before any imports (direct or transitive) that rely on DOM built-ins being
6 | * available, such as `@angular/elements`.
7 | */
8 | import '@angular/platform-server/init';
9 |
10 | import { enableProdMode } from '@angular/core';
11 |
12 | import { environment } from './environments/environment';
13 |
14 | if (environment.production) {
15 | enableProdMode();
16 | }
17 |
18 | export { AppServerModule } from './app/app.server.module';
19 | export { renderModule } from '@angular/platform-server';
20 |
--------------------------------------------------------------------------------
/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-skeleton",
3 | "short_name": "angular-skeleton",
4 | "theme_color": "#1c1918",
5 | "background_color": "#f5f5f4",
6 | "display": "standalone",
7 | "scope": "./",
8 | "start_url": "./",
9 | "icons": [
10 | {
11 | "src": "assets/icons/icon-72x72.png",
12 | "sizes": "72x72",
13 | "type": "image/png",
14 | "purpose": "maskable any"
15 | },
16 | {
17 | "src": "assets/icons/icon-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png",
20 | "purpose": "maskable any"
21 | },
22 | {
23 | "src": "assets/icons/icon-128x128.png",
24 | "sizes": "128x128",
25 | "type": "image/png",
26 | "purpose": "maskable any"
27 | },
28 | {
29 | "src": "assets/icons/icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image/png",
32 | "purpose": "maskable any"
33 | },
34 | {
35 | "src": "assets/icons/icon-152x152.png",
36 | "sizes": "152x152",
37 | "type": "image/png",
38 | "purpose": "maskable any"
39 | },
40 | {
41 | "src": "assets/icons/icon-192x192.png",
42 | "sizes": "192x192",
43 | "type": "image/png",
44 | "purpose": "maskable any"
45 | },
46 | {
47 | "src": "assets/icons/icon-384x384.png",
48 | "sizes": "384x384",
49 | "type": "image/png",
50 | "purpose": "maskable any"
51 | },
52 | {
53 | "src": "assets/icons/icon-512x512.png",
54 | "sizes": "512x512",
55 | "type": "image/png",
56 | "purpose": "maskable any"
57 | }
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/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 recent versions of Safari, Chrome (including
12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE and Safari requires the following polyfill to support IntersectionObserver */
22 | import 'intersection-observer';
23 | /**
24 | * By default, zone.js will patch all possible macroTask and DomEvents
25 | * user can disable parts of macroTask/DomEvents patch by setting following flags
26 | * because those flags need to be set before `zone.js` being loaded, and webpack
27 | * will put import in the top of bundle, so user need to create a separate file
28 | * in this directory (for example: zone-flags.ts), and put the following flags
29 | * into that file, and then add the following code before importing zone.js.
30 | * import './zone-flags';
31 | *
32 | * The flags allowed in zone-flags.ts are listed here.
33 | *
34 | * The following flags will work for all browsers.
35 | *
36 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
37 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
38 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
39 | *
40 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
41 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
42 | *
43 | * (window as any).__Zone_enable_cross_context_check = true;
44 | *
45 | */
46 | /***************************************************************************************************
47 | * Zone JS is required by default for Angular itself.
48 | */
49 | import 'zone.js'; // Included with Angular CLI.
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/src/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 |
3 | # Allow crawling of all content
4 | User-agent: *
5 | Disallow:
6 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import './styles/main';
2 |
--------------------------------------------------------------------------------
/src/styles/abstracts/_functions.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all application-wide Sass functions.
3 | // -----------------------------------------------------------------------------
4 |
--------------------------------------------------------------------------------
/src/styles/abstracts/_mixins.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all application-wide Sass mixins.
3 | // -----------------------------------------------------------------------------
4 |
--------------------------------------------------------------------------------
/src/styles/abstracts/_variables.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all application-wide Sass variables.
3 | // -----------------------------------------------------------------------------
4 |
--------------------------------------------------------------------------------
/src/styles/base/_base.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains very basic styles.
3 | // -----------------------------------------------------------------------------
4 |
5 | html {
6 | @apply scroll-smooth;
7 | }
8 |
9 | body {
10 | @apply m-0 bg-stone-100 bg-cover bg-center bg-no-repeat text-lg text-stone-900 dark:bg-stone-900 dark:text-stone-100;
11 | }
12 |
13 | app-header,
14 | app-navbar,
15 | app-content,
16 | app-footer {
17 | @apply border-2 border-solid border-stone-600 p-5 border-sketch-1 dark:border-stone-100;
18 | }
19 |
--------------------------------------------------------------------------------
/src/styles/base/_fonts.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all @font-face declarations, if any.
3 | // -----------------------------------------------------------------------------
4 |
5 | @import 'https://fonts.googleapis.com/css2?family=Comic+Neue:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap';
6 |
--------------------------------------------------------------------------------
/src/styles/base/_helpers.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains CSS helper classes.
3 | // -----------------------------------------------------------------------------
4 |
--------------------------------------------------------------------------------
/src/styles/base/_typography.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Basic typography style for copy text
3 | */
4 |
5 | body {
6 | @apply font-sans text-neutral-700;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/components/_button.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the button component.
3 | // -----------------------------------------------------------------------------
4 |
5 | .btn {
6 | @apply border-2 border-solid border-stone-600 px-3 py-2.5 font-bold border-sketch-2 dark:border-stone-100;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/layout/_content.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the content of the site/application.
3 | // -----------------------------------------------------------------------------
4 |
5 | app-content {
6 | @apply order-4 grow px-5 md:w-3/4 lg:w-5/6;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/layout/_footer.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the footer of the site/application.
3 | // -----------------------------------------------------------------------------
4 |
5 | app-footer {
6 | @apply order-5 mt-auto p-5;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/layout/_header.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the header of the site/application.
3 | // -----------------------------------------------------------------------------
4 |
5 | app-header {
6 | @apply order-1 text-center;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/layout/_navbar.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the navbar of the site/application.
3 | // -----------------------------------------------------------------------------
4 |
5 | app-navbar {
6 | @apply order-3 px-5 md:w-1/4 lg:w-1/6;
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/layout/_root.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the root component of the site/application.
3 | // -----------------------------------------------------------------------------
4 |
5 | app-root {
6 | @apply flex h-screen flex-col p-5;
7 |
8 | section {
9 | @apply order-2 my-5 flex w-full flex-1 flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | // 1. Configuration and helpers
4 | @import 'abstracts/variables', 'abstracts/functions', 'abstracts/mixins';
5 |
6 | // 2. Vendors
7 | @import 'vendors/tailwind', 'vendors/google';
8 |
9 | // 3. Base stuff
10 | @import 'base/base', 'base/fonts', 'base/typography', 'base/helpers';
11 |
12 | // 4. Layout-related sections
13 | @import 'layout/root', 'layout/header', 'layout/navbar', 'layout/content', 'layout/footer';
14 |
15 | // 5. Components
16 | @import 'components/button';
17 |
--------------------------------------------------------------------------------
/src/styles/vendors/_google.scss:
--------------------------------------------------------------------------------
1 | @import 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap';
2 |
3 | .material-symbols-outlined {
4 | font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/vendors/_tailwind.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to Tailwind CSS.
3 | // -----------------------------------------------------------------------------
4 |
5 | @import 'tailwindcss/base';
6 | @import 'tailwindcss/components';
7 | @import 'tailwindcss/utilities';
8 |
--------------------------------------------------------------------------------
/src/types/figlet/importable-fonts.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for figlet 1.5
2 | // Project: https://github.com/patorjk/figlet.js
3 | // Definitions by: DefinitelyTyped
4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 |
6 | declare module 'figlet/importable-fonts/*' {
7 | const value: string;
8 | export default value;
9 | }
10 |
--------------------------------------------------------------------------------
/src/types/globals.d.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | import 'jest-extended';
3 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['stylelint-config-standard', 'stylelint-config-recommended-scss', 'stylelint-prettier/recommended'],
3 | plugins: ['stylelint-scss', 'stylelint-no-unsupported-browser-features', 'stylelint-prettier'],
4 | rules: {
5 | 'prettier/prettier': true,
6 | 'selector-no-qualifying-type': null,
7 | 'max-nesting-depth': null,
8 | 'at-rule-no-unknown': null,
9 | 'at-rule-empty-line-before': null,
10 | 'no-descending-specificity': null,
11 | 'selector-max-compound-selectors': 5,
12 | 'no-empty-source': null,
13 | 'selector-pseudo-element-no-unknown': [
14 | true,
15 | {
16 | ignorePseudoElements: ['ng-deep']
17 | }
18 | ],
19 | 'plugin/no-unsupported-browser-features': [
20 | true,
21 | {
22 | severity: 'warning'
23 | }
24 | ],
25 | 'scss/at-rule-no-unknown': [
26 | true,
27 | {
28 | ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen']
29 | }
30 | ],
31 | 'import-notation': null
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 | const plugin = require('tailwindcss/plugin');
3 |
4 | module.exports = {
5 | prefix: '',
6 | mode: 'jit',
7 | content: ['./src/**/*.{html,ts,css,scss,sass,less,styl}'],
8 | darkMode: 'class',
9 | theme: {
10 | extend: {
11 | fontFamily: {
12 | sans: ['Comic Neue', ...defaultTheme.fontFamily.sans]
13 | },
14 | backgroundImage: {
15 | waves: "url('/assets/images/background.svg')"
16 | },
17 | colors: {
18 | 'transparent-gray-50': '#cdcdcd52',
19 | 'transparent-gray-100': '#cdcdcd99'
20 | }
21 | }
22 | },
23 | corePlugins: {
24 | preflight: true
25 | },
26 | variants: {
27 | extend: {}
28 | },
29 | plugins: [
30 | require('@tailwindcss/forms'),
31 | require('@tailwindcss/typography'),
32 | require('@tailwindcss/aspect-ratio'),
33 | plugin(function ({ addUtilities }) {
34 | addUtilities({
35 | '.border-sketch-1': {
36 | 'border-radius': '50px 15px 225px 15px/15px 225px 15px 20px'
37 | },
38 | '.border-sketch-2': {
39 | 'border-radius': '255px 15px 225px 15px/15px 225px 15px 255px'
40 | }
41 | });
42 | })
43 | ]
44 | };
45 |
--------------------------------------------------------------------------------
/transloco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rootTranslationsPath: 'assets/i18n/',
3 | langs: ['en', 'es'],
4 | keysManager: {}
5 | };
6 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./out-tsc/app",
4 | "types": []
5 | },
6 | "extends": "./tsconfig.json",
7 | "files": ["src/main.browser.ts", "src/polyfills.ts"],
8 | "include": ["src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "angularCompilerOptions": {
3 | "enableI18nLegacyMessageIdFormat": false,
4 | "strictInjectionParameters": true,
5 | "strictInputAccessModifiers": true,
6 | "strictTemplates": true
7 | },
8 | "compileOnSave": false,
9 | "compilerOptions": {
10 | "baseUrl": "./",
11 | "declaration": false,
12 | "downlevelIteration": true,
13 | "experimentalDecorators": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "importHelpers": true,
16 | "lib": ["es2022", "dom"],
17 | "module": "es2022",
18 | "moduleResolution": "node",
19 | "noFallthroughCasesInSwitch": true,
20 | "noImplicitOverride": true,
21 | "noImplicitReturns": true,
22 | "useDefineForClassFields": false,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "skipLibCheck": true,
26 | "noPropertyAccessFromIndexSignature": true,
27 | "allowSyntheticDefaultImports": true,
28 | "resolveJsonModule": true,
29 | "outDir": "./dist/out-tsc",
30 | "paths": {
31 | "@app/*": ["src/app/*"],
32 | "@core/*": ["src/app/core/*"],
33 | "@env/*": ["src/environments/*"],
34 | "@features/*": ["src/app/features/*"],
35 | "@shared/*": ["src/app/shared/*"]
36 | },
37 | "sourceMap": true,
38 | "strict": true,
39 | "target": "es2022",
40 | "typeRoots": ["node_modules/@types", "src/types"]
41 | },
42 | "files": ["server.ts"],
43 | "include": ["src/**/*"]
44 | }
45 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "angularCompilerOptions": {
3 | "entryModule": "./src/app/app.server.module#AppServerModule"
4 | },
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/server",
7 | "types": ["node"]
8 | },
9 | "extends": "./tsconfig.app.json",
10 | "files": ["src/main.server.ts", "server.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./out-tsc/spec",
4 | "allowJs": true,
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "extends": "./tsconfig.json",
9 | "files": ["src/polyfills.ts"],
10 | "include": ["src/**/*.int.ts", "src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.d.ts"]
11 | }
12 |
--------------------------------------------------------------------------------