├── .browserslistrc ├── .dockerignore ├── .eslintignore ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintignore ├── .stylelintrc.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.es-CO.md ├── README.md ├── angular.json ├── cypress.config.ts ├── cypress ├── e2e │ ├── auth.cy.ts │ └── home.cy.ts ├── fixtures │ └── example.json ├── support │ ├── command.d.ts │ ├── commands.ts │ ├── component.d.ts │ ├── component.ts │ └── e2e.ts └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── scripts ├── docker_cleanup.sh └── rename_project.sh ├── src ├── app │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ ├── app.routes.ts │ ├── lib │ │ ├── components │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.spec.ts │ │ │ │ └── footer.component.ts │ │ │ ├── index.ts │ │ │ ├── layouts │ │ │ │ └── layout-horizontal │ │ │ │ │ ├── layout-horizontal.component.html │ │ │ │ │ ├── layout-horizontal.component.spec.ts │ │ │ │ │ └── layout-horizontal.component.ts │ │ │ ├── logo │ │ │ │ ├── logo.component.html │ │ │ │ ├── logo.component.spec.ts │ │ │ │ └── logo.component.ts │ │ │ └── navbar │ │ │ │ ├── navbar.component.html │ │ │ │ ├── navbar.component.spec.ts │ │ │ │ └── navbar.component.ts │ │ ├── constants.ts │ │ ├── enums │ │ │ ├── indext.ts │ │ │ └── user-role.enum.ts │ │ ├── guards │ │ │ ├── auth.guard.spec.ts │ │ │ ├── auth.guard.ts │ │ │ └── index.ts │ │ ├── interceptors │ │ │ ├── index.ts │ │ │ ├── jwt.interceptor.ts │ │ │ └── server-error.interceptor.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── user.interface.ts │ │ ├── providers │ │ │ ├── index.ts │ │ │ └── package-json.token.ts │ │ ├── services │ │ │ ├── auth │ │ │ │ ├── auth.service.spec.ts │ │ │ │ └── auth.service.ts │ │ │ ├── index.ts │ │ │ └── theme │ │ │ │ ├── index.ts │ │ │ │ ├── theme.config.ts │ │ │ │ ├── theme.service.spec.ts │ │ │ │ └── theme.service.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── storage │ │ │ ├── storage.types.ts │ │ │ ├── storage.utils.spec.ts │ │ │ └── storage.utils.ts │ └── pages │ │ ├── auth │ │ ├── index.ts │ │ ├── login │ │ │ ├── login.component.html │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ │ └── register │ │ │ ├── register.component.html │ │ │ ├── register.component.spec.ts │ │ │ └── register.component.ts │ │ ├── home │ │ ├── home.component.html │ │ ├── home.component.spec.ts │ │ ├── home.component.ts │ │ └── index.ts │ │ ├── screens │ │ ├── index.ts │ │ └── not-found │ │ │ ├── not-found.component.html │ │ │ ├── not-found.component.spec.ts │ │ │ └── not-found.component.ts │ │ ├── settings │ │ ├── accessibility │ │ │ ├── accessibility.component.html │ │ │ ├── accessibility.component.spec.ts │ │ │ └── accessibility.component.ts │ │ ├── account │ │ │ ├── account.component.html │ │ │ ├── account.component.spec.ts │ │ │ └── account.component.ts │ │ ├── appearance │ │ │ ├── appearance.component.html │ │ │ ├── appearance.component.spec.ts │ │ │ └── appearance.component.ts │ │ └── index.ts │ │ └── user │ │ ├── index.ts │ │ └── profile │ │ ├── profile.component.html │ │ ├── profile.component.spec.ts │ │ └── profile.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.development.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts └── theme │ ├── .gitkeep │ ├── 01-base │ ├── .gitkeep │ ├── document.css │ ├── font.css │ ├── heading.css │ └── variables.css │ ├── 02-components │ ├── .gitkeep │ ├── button.css │ └── link.css │ ├── 03-utilities │ ├── .gitkeep │ └── border.css │ ├── styles.css │ └── tailwindcss │ ├── base.css │ ├── components.css │ └── utilities.css ├── tailwind.config.ts ├── tsconfig.app.json ├── tsconfig.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 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules 2 | node_modules 3 | 4 | # Ignore any file or directory that starts with a dot 5 | .* 6 | 7 | # Ignore development files 8 | *.log 9 | *.swp 10 | *.bak 11 | *.backup 12 | *.tmp 13 | 14 | # Ignore test files 15 | test 16 | tests 17 | *.test.* 18 | *.spec.* 19 | 20 | # Ignore build files 21 | dist 22 | build 23 | *.map 24 | *.tsbuildinfo 25 | 26 | # Ignore package manager cache 27 | .pnpm-store 28 | .yarn-cache 29 | 30 | # Ignore configuration files 31 | /envs 32 | 33 | # Ignore editor files 34 | .vscode 35 | .idea 36 | *.iml 37 | 38 | # Ignore Github files 39 | .github 40 | 41 | # Ignore k8s files 42 | /charts 43 | 44 | # Ignore documentation files 45 | CHANGELOG 46 | LICENSE 47 | AUTHORS 48 | CONTRIBUTORS 49 | 50 | # Ignore version control files 51 | .git 52 | .gitignore 53 | .svn 54 | .hg 55 | .bzr 56 | 57 | # Ignore OS-specific files 58 | .DS_Store 59 | Thumbs.db 60 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | **/build 5 | /dist 6 | **/dist 7 | .env 8 | .env.* 9 | !.env.example 10 | __generated__ 11 | 12 | # Ignore files for PNPM, NPM and YARN 13 | pnpm-lock.yaml 14 | package-lock.json 15 | yarn.lock 16 | 17 | # Ignore test coverage 18 | **/coverage 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "plugin:@angular-eslint/recommended", 12 | "plugin:@angular-eslint/template/process-inline-templates", 13 | "plugin:prettier/recommended" 14 | ], 15 | "parserOptions": { 16 | "project": true, 17 | "tsconfigRootDir": "./" 18 | }, 19 | "rules": { 20 | "@angular-eslint/directive-selector": [ 21 | "error", 22 | { 23 | "type": "attribute", 24 | "prefix": "app", 25 | "style": "camelCase" 26 | } 27 | ], 28 | "@angular-eslint/component-selector": [ 29 | "error", 30 | { 31 | "type": "element", 32 | "prefix": "app", 33 | "style": "kebab-case" 34 | } 35 | ], 36 | "@typescript-eslint/no-floating-promises": "off", 37 | "@typescript-eslint/no-unused-vars": "error", 38 | "@typescript-eslint/explicit-function-return-type": "error", 39 | "@typescript-eslint/no-explicit-any": "error", 40 | "@typescript-eslint/naming-convention": [ 41 | "error", 42 | { 43 | "selector": "variable", 44 | "modifiers": ["const"], 45 | "format": ["strictCamelCase", "UPPER_CASE"] 46 | }, 47 | { 48 | "selector": "variable", 49 | "types": ["boolean"], 50 | "format": ["StrictPascalCase"], 51 | "prefix": ["is", "should", "has", "can", "did", "will"] 52 | }, 53 | { 54 | "selector": "function", 55 | "format": ["strictCamelCase"] 56 | }, 57 | { 58 | "selector": "parameter", 59 | "format": ["strictCamelCase"], 60 | "leadingUnderscore": "allow" 61 | }, 62 | { 63 | "selector": "memberLike", 64 | "format": ["strictCamelCase"] 65 | }, 66 | { 67 | "selector": "memberLike", 68 | "modifiers": ["private"], 69 | "format": ["strictCamelCase"], 70 | "leadingUnderscore": "require" 71 | }, 72 | { 73 | "selector": "typeLike", 74 | "format": ["StrictPascalCase"] 75 | }, 76 | { 77 | "selector": "enum", 78 | "format": ["StrictPascalCase"] 79 | }, 80 | { 81 | "selector": "enumMember", 82 | "format": ["StrictPascalCase"] 83 | } 84 | ] 85 | } 86 | }, 87 | { 88 | "files": ["*.html"], 89 | "extends": ["plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility"], 90 | "rules": {} 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "singleAttributePerLine": true, 6 | "printWidth": 120, 7 | "tabWidth": 4, 8 | "useTabs": false, 9 | "endOfLine": "auto", 10 | "overrides": [ 11 | { 12 | "files": ["*.md", "*.json", "*.yml", "*.yaml"], 13 | "options": { 14 | "tabWidth": 2 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | **/build 2 | **/coverage 3 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@culur/stylelint-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template", "bradlc.vscode-tailwindcss"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true 4 | }, 5 | "[html]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[markdown]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[handlebars]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[css]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[less]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[scss]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[javascript]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode" 25 | }, 26 | "[typescript]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | }, 29 | "[json]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | }, 32 | "[jsonc]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "[dockerfile]": { 36 | "editor.defaultFormatter": "ms-azuretools.vscode-docker" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ;-------------; 2 | # ; Build stage ; 3 | # ;-------------; 4 | FROM node:20-alpine as builder 5 | 6 | WORKDIR /app 7 | 8 | RUN npm i -g pnpm 9 | 10 | COPY . . 11 | 12 | RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \ 13 | pnpm install --frozen-lockfile && \ 14 | pnpm build 15 | 16 | # ;---------------; 17 | # ; Runtime stage ; 18 | # ;---------------; 19 | FROM nginx:stable-alpine as runtime 20 | 21 | COPY --from=builder /app/dist/angular-boilerplate /usr/share/nginx/html 22 | 23 | EXPOSE 80 24 | 25 | CMD ["nginx", "-g", "daemon off;"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-PRESENT Juan Mesa 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 | -------------------------------------------------------------------------------- /README.es-CO.md: -------------------------------------------------------------------------------- 1 |

2 | Angular brand 3 |

4 | 5 |

Angular Boilerplate

6 | 7 |
8 | 9 |

10 | Sitio en vivo (Demo) 11 |

12 | 13 |
14 | 15 |

16 | English | 17 | Español 18 |

19 | 20 |
21 | 22 | Este es un proyecto que se enfoca en las últimas características y mejores prácticas de Angular. Ofrece características esenciales para flexibilidad y escalabilidad, minimizando la sobrecarga innecesaria. El código es liviano pero robusto, permitiendo a los desarrolladores elegir sus tecnologías preferidas, como bibliotecas de componentes de interfaz de usuario, gestión del estado, renderización en el servidor, etc. Su esquema flexible permite una personalización y adaptación sencilla a los requisitos únicos del proyecto. 23 | 24 | ## ⚗️ Features 25 | 26 | - [Angular 16](https://angular.io/docs) 27 | - [PNPM](https://pnpm.io/), [esbuild](https://esbuild.github.io/) 28 | - [Components independientes](https://angular.io/guide/standalone-components) 29 | - [Señales](https://angular.io/guide/signals) 30 | - [Carga diferida](https://angular.io/guide/lazy-loading-ngmodules) 31 | - [PWA](https://angular.io/guide/service-worker-getting-started) 32 | - [I18n](https://ngneat.github.io/transloco/) 33 | - [TailwindCSS](https://tailwindcss.com/) 34 | - Temas OS/Light/Dark 35 | - Liviano, rápido y construído con tecnología de última generación. 36 | 37 | ## ✅ Listo para usar 38 | 39 | ### Marcos de IU 40 | 41 | - [TailwindCSS](https://tailwindcss.com/) 42 | 43 | ### Íconos 44 | 45 | - [Iconify](https://iconify.design) - usar íconos de cualquier conjunto de íconos [🔍Icônes](https://icones.netlify.app/). 46 | - [@iconify/tailwind](https://docs.iconify.design/usage/css/tailwind/) - Íconos en CSS puro. 47 | 48 | ### Complementos 49 | 50 | - 51 | - 52 | - 53 | - 54 | - 55 | - 56 | - 57 | - 58 | 59 | ## ⚙ Requisitos previos 60 | 61 | - Node.js ([^16.14.0 || ^18.10.0](https://angular.io/guide/versions)): 62 | - PNPM: 63 | - Docker (opcional): 64 | 65 | ## 🏹 Iniciar desarrollo 66 | 67 | > **Nota:** 68 | > Tiene tres opciones para comenzar un nuevo proyecto basado en esta plantilla: 69 | > 70 | > 1. Crear un nuevo repositorio de GitHub a partir de esta plantilla. 71 | > 2. Clonar este repositorio para comenzar con un historial de git limpio. 72 | > 3. Crear un fork del proyecto en StackBlitz. 73 | 74 | ### Utilizando plantilla de GitHub 75 | 76 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/juanmesa2097/angular-boilerplate) 77 | 78 | --- 79 | 80 | ### Clonando el repositorio localmente 81 | 82 | ```sh 83 | npx degit juanmesa2097/angular-boilerplate my-app && cd my-app && ./scripts/rename_project.sh my-app 84 | ``` 85 | 86 | ### Instalar dependencias 87 | 88 | ```sh 89 | pnpm install # run `pnpm install -g pnpm` if you don't have pnpm installed 90 | ``` 91 | 92 | ### Ejecutar proyecto 93 | 94 | ```sh 95 | pnpm dev 96 | ``` 97 | 98 | --- 99 | 100 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://analogjs.org/new) 101 | 102 | ## 📝 Checklist 103 | 104 | Por favor revise esta lista de verificación y modifíquela según sea necesario para cumplir con los requisitos de su proyecto. 105 | 106 | - [ ] Ejecute el script `./scripts/rename_project.sh` para renombrar el proyecto. 107 | - [ ] Cambie el título en `src/index.html` y el favicon en `src/favicon.ico` para que coincidan con su proyecto. 108 | - [ ] Decida si desea continuar utilizando [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) y [lint-staged](https://github.com/okonet/lint-staged) para su proyecto. 109 | - [ ] Limpie el archivo README para proporcionar instrucciones claras sobre su proyecto. 110 | - [ ] Modifique las páginas del proyecto para cumplir con sus requisitos específicos. 111 | 112 | ## 📦 Despliegue en Vercel 113 | 114 | [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/juanmesa2097/angular-boilerplate) 115 | 116 | ## 📦 Despliegue en Netlify 117 | 118 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/juanmesa2097/angular-boilerplate) 119 | 120 | ## 🐳 Docker 121 | 122 | Crear una imagen del proyecto. 123 | 124 | ```sh 125 | docker buildx build -t angular-boilerplate:latest . 126 | ``` 127 | 128 | Ejecutar la imagen del proyecto. 129 | 130 | ```sh 131 | docker run --rm -p 8080:80 -d angular-boilerplate:latest 132 | ``` 133 | 134 | ## 🧙‍♂️ Comandos 135 | 136 | | Comando | Descripción | npm | yarn | pnpm | 137 | | --------------- | ------------------------------------------------------------------------- | ----------------------- | -------------------- | -------------------- | 138 | | `dev` | Inicia el servidor de desarrollo | `npm start` | `yarn start` | `pnpm start` | 139 | | `dev:host` | Inicia el servidor de desarrollo con un host personalizado | `npm start` | `yarn start` | `pnpm start` | 140 | | `build` | Compila el código de producción | `npm run build` | `yarn build` | `pnpm build` | 141 | | `watch` | Compila el código de producción y lo vigila para detectar cambios | `npm run watch` | `yarn watch` | `pnpm watch` | 142 | | `test` | Ejecuta las pruebas unitarias | `npm run test` | `yarn test` | `pnpm test` | 143 | | `test:headless` | Ejecuta las pruebas unitarias en modo sin cabeza | `npm run test:headless` | `yarn test:headless` | `pnpm test:headless` | 144 | | `lint` | Ejecuta el linter | `npm run lint` | `yarn lint` | `pnpm lint` | 145 | | `lint:fix` | Ejecuta el linter y corrige cualquier error de lint | `npm run lint:fix` | `yarn lint:fix` | `pnpm lint:fix` | 146 | | `lint:staged` | Ejecuta el linter en los archivos en cola | `npm run lint:staged` | `yarn lint:staged` | `pnpm lint:staged` | 147 | | `stylelint` | Ejecuta el linter de estilos | `npm run stylelint` | `yarn stylelint` | `pnpm stylelint` | 148 | | `stylelint:fix` | Ejecuta el linter de estilos y corrige cualquier error de lint de estilos | `npm run stylelint:fix` | `yarn stylelint:fix` | `pnpm stylelint:fix` | 149 | | `format` | Formatea el código con Prettier | `npm run format` | `yarn format` | `pnpm format` | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Angular brand 3 |

4 | 5 |

Angular Boilerplate

6 | 7 |
8 | 9 |

10 | Live site (Demo) 11 |

12 | 13 |
14 | 15 |

16 | English | 17 | Español 18 |

19 | 20 |
21 | 22 | This opinionated Angular starter focuses on the latest Angular features and best practices. It offers essential features for flexibility and scalability, minimizing unnecessary overhead. The codebase is lightweight yet robust, allowing developers to choose their preferred technologies like UI component libraries, state management, server-side rendering, etc. Its flexible boilerplate enables easy customization and adaptation to unique project requirements. 23 | 24 | ## ⚗️ Features 25 | 26 | - [Angular 16](https://angular.io/docs) 27 | - [PNPM](https://pnpm.io/), [esbuild](https://esbuild.github.io/) 28 | - [Standalone components](https://angular.io/guide/standalone-components) 29 | - [Signals](https://angular.io/guide/signals) 30 | - [Lazy loading](https://angular.io/guide/lazy-loading-ngmodules) 31 | - [PWA](https://angular.io/guide/service-worker-getting-started) 32 | - [I18n](https://ngneat.github.io/transloco/) 33 | - [TailwindCSS](https://tailwindcss.com/) 34 | - OS/Light/Dark themes 35 | - Lightweight, fast, and built using state-of-the-art technology. 36 | 37 | ## ✅ Ready-to-use 38 | 39 | ### UI Frameworks 40 | 41 | - [TailwindCSS](https://tailwindcss.com/) 42 | 43 | ### Icons 44 | 45 | - [Iconify](https://iconify.design) - use icons from any icon sets [🔍Icônes](https://icones.netlify.app/). 46 | - [@iconify/tailwind](https://docs.iconify.design/usage/css/tailwind/) - Pure CSS icons. 47 | 48 | ### Add-ons 49 | 50 | - 51 | - 52 | - 53 | - 54 | - 55 | - 56 | - 57 | - 58 | 59 | ## ⚙ Prerequisites 60 | 61 | - Node.js ([^16.14.0 || ^18.10.0](https://angular.io/guide/versions)): 62 | - PNPM: 63 | - Docker (optional): 64 | 65 | ## 🏹 Start development 66 | 67 | > **Note:** 68 | > You have three options to start a new project based on this template: 69 | > 70 | > 1. Create a new GitHub repository from this template. 71 | > 2. Clone this repository to start with a clean git history. 72 | > 3. Scaffold a project fork on StackBlitz. 73 | 74 | ### Using the GitHub template 75 | 76 | [Create a repo from this template on GitHub](https://github.com/juanmesa2097/angular-boilerplate/generate). 77 | 78 | --- 79 | 80 | ### Cloning the repository locally 81 | 82 | ```sh 83 | npx degit juanmesa2097/angular-boilerplate my-app && cd my-app && ./scripts/rename_project.sh my-app 84 | ``` 85 | 86 | ### Install dependencies 87 | 88 | ```sh 89 | pnpm install # run `npm install -g pnpm` if you don't have pnpm installed 90 | ``` 91 | 92 | ### Run project 93 | 94 | ```sh 95 | pnpm dev 96 | ``` 97 | 98 | --- 99 | 100 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/juanmesa2097/angular-boilerplate) 101 | 102 | ## 📝 Checklist 103 | 104 | Please review this checklist and modify it as necessary to meet your project requirements. 105 | 106 | - [ ] Run the `./scripts/rename_project.sh` script to rename the project. 107 | - [ ] Change the title in `src/index.html` and the favicon in `src/favicon.ico` to match your project. 108 | - [ ] Decide whether to continue using [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) and [lint-staged](https://github.com/okonet/lint-staged) for your project. 109 | - [ ] Clean up the README file to provide clear instructions about your project. 110 | - [ ] Modify the pages in the project to meet your specific requirements. 111 | 112 | ## 📦 Deploy to Vercel 113 | 114 | [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/juanmesa2097/angular-boilerplate) 115 | 116 | ## 📦 Deploy to Netlify 117 | 118 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/juanmesa2097/angular-boilerplate) 119 | 120 | ## 🐳 Docker 121 | 122 | Create an image of the project. 123 | 124 | ```sh 125 | docker buildx build -t angular-boilerplate:latest . 126 | ``` 127 | 128 | Run the image of the project. 129 | 130 | ```sh 131 | docker run --rm -p 8080:80 -d angular-boilerplate:latest 132 | ``` 133 | 134 | ## 🧙‍♂️ Commands 135 | 136 | | Command | Description | npm | yarn | pnpm | 137 | | --------------- | -------------------------------------------------------- | ----------------------- | -------------------- | -------------------- | 138 | | `dev` | Starts the development server | `npm run dev` | `yarn dev` | `pnpm dev` | 139 | | `dev:host` | Starts the development server with a custom host | `npm run dev` | `yarn dev` | `pnpm dev` | 140 | | `build` | Builds the production code | `npm run build` | `yarn build` | `pnpm build` | 141 | | `watch` | Builds the production code and watches for changes | `npm run watch` | `yarn watch` | `pnpm watch` | 142 | | `test` | Runs the unit tests | `npm run test` | `yarn test` | `pnpm test` | 143 | | `test:e2e` | Open Cypress | `npm run test` | `yarn test` | `pnpm test` | 144 | | `test:headless` | Runs the unit tests in headless mode | `npm run test:headless` | `yarn test:headless` | `pnpm test:headless` | 145 | | `lint` | Runs the linter | `npm run lint` | `yarn lint` | `pnpm lint` | 146 | | `lint:fix` | Runs the linter and fixes any linting errors | `npm run lint:fix` | `yarn lint:fix` | `pnpm lint:fix` | 147 | | `lint:staged` | Runs the linter on staged files | `npm run lint:staged` | `yarn lint:staged` | `pnpm lint:staged` | 148 | | `stylelint` | Runs the style linter | `npm run stylelint` | `yarn stylelint` | `pnpm stylelint` | 149 | | `stylelint:fix` | Runs the style linter and fixes any style linting errors | `npm run stylelint:fix` | `yarn stylelint:fix` | `pnpm stylelint:fix` | 150 | | `format` | Formats the code with prettier | `npm run format` | `yarn format` | `pnpm format` | 151 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-boilerplate": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "standalone": true 11 | }, 12 | "@schematics/angular:directive": { 13 | "standalone": true 14 | }, 15 | "@schematics/angular:pipe": { 16 | "standalone": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser-esbuild", 25 | "options": { 26 | "outputPath": "dist/angular-boilerplate", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": ["zone.js"], 30 | "tsConfig": "tsconfig.app.json", 31 | "assets": ["src/favicon.ico", "src/assets"], 32 | "styles": ["src/theme/styles.css"], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "budgets": [ 38 | { 39 | "type": "initial", 40 | "maximumWarning": "500kb", 41 | "maximumError": "1mb" 42 | }, 43 | { 44 | "type": "anyComponentStyle", 45 | "maximumWarning": "2kb", 46 | "maximumError": "4kb" 47 | } 48 | ], 49 | "outputHashing": "all" 50 | }, 51 | "development": { 52 | "buildOptimizer": false, 53 | "optimization": false, 54 | "vendorChunk": true, 55 | "extractLicenses": false, 56 | "sourceMap": true, 57 | "namedChunks": true, 58 | "fileReplacements": [ 59 | { 60 | "replace": "src/environments/environment.ts", 61 | "with": "src/environments/environment.development.ts" 62 | } 63 | ] 64 | } 65 | }, 66 | "defaultConfiguration": "production" 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "angular-boilerplate:build:production" 73 | }, 74 | "development": { 75 | "browserTarget": "angular-boilerplate:build:development" 76 | } 77 | }, 78 | "defaultConfiguration": "development" 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "angular-boilerplate:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "polyfills": ["zone.js", "zone.js/testing"], 90 | "tsConfig": "tsconfig.spec.json", 91 | "assets": ["src/favicon.ico", "src/assets"], 92 | "styles": ["src/theme/styles.css"], 93 | "scripts": [] 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-eslint/builder:lint", 98 | "options": { 99 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "cli": { 106 | "schematicCollections": ["@angular-eslint/schematics"] 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:4200', 6 | setupNodeEvents(on, config) { 7 | // implement node event listeners here 8 | }, 9 | experimentalRunAllSpecs: true, 10 | }, 11 | component: { 12 | devServer: { 13 | framework: 'angular', 14 | bundler: 'webpack', 15 | }, 16 | specPattern: '**/*.cy.ts', 17 | }, 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /cypress/e2e/auth.cy.ts: -------------------------------------------------------------------------------- 1 | describe('auth flow', () => { 2 | it('Succesfully redirect if no logged in', () => { 3 | cy.visit('/'); 4 | cy.location('pathname').should('eq', '/auth/login'); 5 | }); 6 | 7 | it('Succesfully redirect to home when log in', () => { 8 | cy.login(); 9 | cy.location('pathname').should('eq', '/'); 10 | }); 11 | 12 | it('succesfully route to home if user is present in storage', () => { 13 | cy.setAuthSession(); 14 | cy.visit('/'); 15 | cy.location('pathname').should('eq', '/'); 16 | }); 17 | 18 | it('succesfully log out if logout button is pressed', () => { 19 | cy.login(); 20 | cy.get('[data-cy="logout-button"]').click(); 21 | cy.location('pathname').should('eq', '/auth/login'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /cypress/e2e/home.cy.ts: -------------------------------------------------------------------------------- 1 | describe('home spec', () => { 2 | it('Succesfully redirect if no logged in', () => { 3 | cy.login(); 4 | cy.location('pathname').should('eq', '/'); 5 | }); 6 | 7 | it('Succesfully switch theme to dark', () => { 8 | cy.login(); 9 | const darkThemeBtn = cy.get('[data-cy="theme-dark-button"]'); 10 | darkThemeBtn.click(); 11 | cy.get('body').should('have.class', 'dark'); 12 | }); 13 | 14 | it('Succesfully switch theme to light', () => { 15 | cy.login(); 16 | const darkThemeBtn = cy.get('[data-cy="theme-light-button"]'); 17 | darkThemeBtn.click(); 18 | cy.get('body').should('have.class', 'light'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/command.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Custom command to type a few random words into input elements 7 | * @param count=3 8 | * @example cy.get('input').typeRandomWords() 9 | */ 10 | login(): Chainable; 11 | setAuthSession(): Chainable; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | 12 | Cypress.Commands.add('login', () => { 13 | cy.visit('/auth/login'); 14 | cy.get('[data-cy="login-button"]').click(); 15 | }); 16 | 17 | Cypress.Commands.add('setAuthSession', () => { 18 | window.localStorage.setItem('appSession', '{"user":"some-user-id","token":"abc"}'); 19 | }); 20 | -------------------------------------------------------------------------------- /cypress/support/component.d.ts: -------------------------------------------------------------------------------- 1 | // Augment the Cypress namespace to include type definitions for 2 | // your custom command. 3 | // Alternatively, can be defined in cypress/support/component.d.ts 4 | // with a at the top of your spec. 5 | declare global { 6 | namespace Cypress { 7 | interface Chainable { 8 | mount: typeof mount; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // Import commands.js using ES2015 syntax: 2 | import './commands'; 3 | 4 | // Alternatively you can use CommonJS syntax: 5 | // require('./commands') 6 | 7 | import { mount } from 'cypress/angular'; 8 | 9 | Cypress.Commands.add('mount', mount); 10 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.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 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "types": ["cypress", "./support"], 5 | "lib": ["ES2022", "dom"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-boilerplate", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "npx simple-git-hooks", 7 | "ng": "ng", 8 | "dev": "ng serve", 9 | "dev:host": "ng serve --host 0.0.0.0 --disable-host-check", 10 | "build": "ng build", 11 | "watch": "ng build --watch --configuration development", 12 | "test": "ng test", 13 | "test:e2e": "cypress open", 14 | "test:headless": "ng test --watch=false --browsers ChromeHeadless", 15 | "lint": "eslint .", 16 | "lint:fix": "eslint . --fix", 17 | "lint:staged": "pnpm lint-staged", 18 | "stylelint": "stylelint **/*.{css,pcss,less,scss,sass} --quiet-deprecation-warnings", 19 | "stylelint:fix": "stylelint **/*.{css,pcss,less,scss,sass} --quiet-deprecation-warnings --fix", 20 | "format": "prettier --write ." 21 | }, 22 | "simple-git-hooks": { 23 | "pre-commit": "pnpm lint-staged", 24 | "pre-push": "pnpm build" 25 | }, 26 | "lint-staged": { 27 | "*.{js,jsx,ts,tsx}": [ 28 | "pnpm lint:fix", 29 | "pnpm prettier" 30 | ], 31 | "*.{css,pcss,less,scss,sass}": [ 32 | "pnpm stylelint:fix", 33 | "pnpm prettier" 34 | ] 35 | }, 36 | "dependencies": { 37 | "@angular/animations": "^16.0.0", 38 | "@angular/common": "^16.0.0", 39 | "@angular/compiler": "^16.0.0", 40 | "@angular/core": "^16.0.0", 41 | "@angular/forms": "^16.0.0", 42 | "@angular/platform-browser": "^16.0.0", 43 | "@angular/platform-browser-dynamic": "^16.0.0", 44 | "@angular/router": "^16.0.0", 45 | "rxjs": "~7.8.0", 46 | "tslib": "^2.3.0", 47 | "zone.js": "~0.13.0" 48 | }, 49 | "devDependencies": { 50 | "@angular-devkit/build-angular": "^16.0.1", 51 | "@angular-eslint/builder": "16.0.1", 52 | "@angular-eslint/eslint-plugin": "16.0.1", 53 | "@angular-eslint/eslint-plugin-template": "16.0.1", 54 | "@angular-eslint/schematics": "16.0.1", 55 | "@angular-eslint/template-parser": "16.0.1", 56 | "@angular/cli": "~16.0.1", 57 | "@angular/compiler-cli": "^16.0.0", 58 | "@culur/stylelint-config": "^1.2.0", 59 | "@iconify/json": "^2.2.64", 60 | "@iconify/tailwind": "^0.1.2", 61 | "@tailwindcss/aspect-ratio": "^0.4.2", 62 | "@tailwindcss/forms": "^0.5.3", 63 | "@tailwindcss/typography": "^0.5.9", 64 | "@types/jasmine": "~4.3.0", 65 | "@types/node": "^20.1.3", 66 | "@typescript-eslint/eslint-plugin": "5.59.2", 67 | "@typescript-eslint/parser": "5.59.2", 68 | "autoprefixer": "^10.4.14", 69 | "cypress": "^13.1.0", 70 | "eslint": "^8.39.0", 71 | "eslint-config-prettier": "^8.8.0", 72 | "eslint-plugin-prettier": "^4.2.1", 73 | "jasmine-core": "~4.6.0", 74 | "karma": "~6.4.0", 75 | "karma-chrome-launcher": "~3.2.0", 76 | "karma-coverage": "~2.2.0", 77 | "karma-jasmine": "~5.1.0", 78 | "karma-jasmine-html-reporter": "~2.0.0", 79 | "lint-staged": "^13.2.2", 80 | "postcss": "^8.4.23", 81 | "prettier": "^2.8.8", 82 | "prettier-eslint": "^15.0.1", 83 | "prettier-plugin-tailwindcss": "^0.2.8", 84 | "simple-git-hooks": "^2.8.1", 85 | "stylelint": "^15.6.1", 86 | "tailwindcss": "^3.3.2", 87 | "typescript": "~5.0.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /scripts/docker_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Stop and remove all containers 4 | if [ "$(docker ps -aq)" ]; then 5 | docker stop $(docker ps -a -q) 6 | docker rm $(docker ps -a -q) 7 | else 8 | echo "No running containers found" 9 | fi 10 | 11 | # Remove all images 12 | if [ "$(docker images -q)" ]; then 13 | docker rmi $(docker images -a -q) 14 | else 15 | echo "No images found" 16 | fi 17 | 18 | # Prune volume 19 | if [ "$(docker volume ls -q)" ]; then 20 | docker volume rm $(docker volume ls -q) 21 | else 22 | echo "No volumes found" 23 | fi 24 | -------------------------------------------------------------------------------- /scripts/rename_project.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | current_project_name="angular-boilerplate" 4 | current_git_provider="github.com/juanmesa2097" 5 | current_git_username="juanmesa2097" 6 | 7 | # Function to find files recursively 8 | find_files() { 9 | local files 10 | files=$(grep -rl --exclude-dir={.git,.angular,node_modules,dist} "$current_project_name" .) 11 | echo "$files" 12 | } 13 | 14 | # Function to prompt for new project name 15 | prompt_project_name() { 16 | read -rp "▶ Enter new project name: [current: $current_project_name] " project_name 17 | project_name=${project_name:-"$current_project_name"} 18 | echo "$project_name" 19 | } 20 | 21 | # Function to prompt for Git provider 22 | prompt_git_provider() { 23 | read -rp "▶ Enter Git provider: [current: $current_git_provider] " git_provider 24 | git_provider=${git_provider:-"$current_git_provider"} 25 | echo "$git_provider" 26 | } 27 | 28 | # Function to prompt for Git username 29 | prompt_git_username() { 30 | read -rp "▶ Enter Git username: [current: $current_git_username] " git_username 31 | git_username=${git_username:-"$current_git_username"} 32 | echo "$git_username" 33 | } 34 | 35 | # Function to prompt for confirmation (Default: n) 36 | prompt_confirmation() { 37 | local project_name=$1 38 | local git_provider=$2 39 | local git_username=$3 40 | 41 | if [[ "$project_name" == "$current_project_name" && "$git_provider" == "$current_git_provider" && "$git_username" == "$current_git_username" ]]; then 42 | echo "The parameters remained unchanged. No renaming and text replacement required." >/dev/tty 43 | exit 0 44 | fi 45 | 46 | echo "You are about to rename the project with the following parameters:" >/dev/tty 47 | echo "Project name:" "$project_name" >/dev/tty 48 | echo "Git provider:" "$git_provider" >/dev/tty 49 | echo "Git username:" "$git_username" >/dev/tty 50 | read -rp "▶ Are you sure you want to continue? (y/n): [n] " confirm 51 | confirm=${confirm:-n} 52 | echo "$confirm" 53 | } 54 | 55 | # Function to process files 56 | process_files() { 57 | local files=$1 58 | local project_name=$2 59 | local git_provider=$3 60 | local git_username=$4 61 | 62 | for file in $files; do 63 | sed -i \ 64 | -e "s|$current_git_provider|$git_provider|g" \ 65 | -e "s|$current_git_username|$git_username|g" \ 66 | -e "s|$current_project_name|$project_name|g" \ 67 | "$file" 68 | done 69 | 70 | echo 71 | echo "✅ Project renamed to $project_name" 72 | echo "✅ Git provider: $git_provider" 73 | echo "✅ Git username: $git_username" 74 | } 75 | 76 | # Main script logic 77 | files=$(find_files) 78 | 79 | # Display files found 80 | echo "Results found for $current_project_name" 81 | echo "$files" 82 | echo 83 | 84 | project_name=$(prompt_project_name) 85 | git_provider=$(prompt_git_provider) 86 | git_username=$(prompt_git_username) 87 | 88 | confirm=$(prompt_confirmation "$project_name" "$git_provider" "$git_username") 89 | 90 | # Process files if confirmed 91 | if [ "$confirm" == "y" ] || [ "$confirm" == "Y" ]; then 92 | process_files "$files" "$project_name" "$git_provider" "$git_username" 93 | else 94 | echo "❌ Renaming and text replacement canceled." 95 | fi 96 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, inject, OnInit } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { AuthService } from '@lib/services'; 5 | import { ThemeService } from '@lib/services/theme'; 6 | import { LayoutHorizontalComponent } from './lib/components/layouts/layout-horizontal/layout-horizontal.component'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | standalone: true, 11 | imports: [CommonModule, RouterModule, LayoutHorizontalComponent], 12 | templateUrl: './app.component.html', 13 | }) 14 | export class AppComponent implements OnInit { 15 | isAuthenticated$ = inject(AuthService).isAuthenticated$; 16 | 17 | private readonly _themeService = inject(ThemeService); 18 | 19 | ngOnInit(): void { 20 | this._themeService.init(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withInterceptors } from '@angular/common/http'; 2 | import { ApplicationConfig } from '@angular/core'; 3 | import { provideRouter, withComponentInputBinding } from '@angular/router'; 4 | import { jwtInterceptor, serverErrorInterceptor } from '@lib/interceptors'; 5 | import { routes } from './app.routes'; 6 | 7 | export const appConfig: ApplicationConfig = { 8 | providers: [ 9 | provideRouter(routes, withComponentInputBinding()), 10 | provideHttpClient(withInterceptors([serverErrorInterceptor, jwtInterceptor])), 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { authGuard } from '@lib/guards'; 3 | 4 | export const routes: Routes = [ 5 | { 6 | path: 'auth', 7 | loadChildren: async () => (await import('@pages/auth')).routes, 8 | canMatch: [authGuard({ requiresAuthentication: false })], 9 | }, 10 | { 11 | path: '', 12 | loadChildren: async () => (await import('@pages/home')).routes, 13 | canMatch: [authGuard()], 14 | }, 15 | { 16 | path: 'users/:username', 17 | loadChildren: async () => (await import('@pages/user')).routes, 18 | canMatch: [authGuard()], 19 | }, 20 | { 21 | path: 'settings', 22 | loadChildren: async () => (await import('@pages/settings')).routes, 23 | canMatch: [authGuard()], 24 | }, 25 | { 26 | path: '**', 27 | loadComponent: async () => (await import('@pages/screens/not-found/not-found.component')).NotFoundComponent, 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/app/lib/components/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /src/app/lib/components/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { FooterComponent } from './footer.component'; 5 | 6 | describe('FooterComponent', () => { 7 | let component: FooterComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [FooterComponent, RouterTestingModule], 13 | }).compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/lib/components/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { PACKAGE_JSON, providePackageJson } from '@lib/providers'; 5 | import { LogoComponent } from '../logo/logo.component'; 6 | 7 | @Component({ 8 | selector: 'app-footer', 9 | standalone: true, 10 | imports: [CommonModule, RouterModule, LogoComponent], 11 | providers: [providePackageJson()], 12 | templateUrl: './footer.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class FooterComponent { 16 | readonly packageJson = inject(PACKAGE_JSON); 17 | readonly currentYear = new Date().getFullYear(); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer/footer.component'; 2 | export * from './layouts/layout-horizontal/layout-horizontal.component'; 3 | export * from './navbar/navbar.component'; 4 | -------------------------------------------------------------------------------- /src/app/lib/components/layouts/layout-horizontal/layout-horizontal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/lib/components/layouts/layout-horizontal/layout-horizontal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { LayoutHorizontalComponent } from './layout-horizontal.component'; 4 | 5 | describe('LayoutHorizontalComponent', () => { 6 | let component: LayoutHorizontalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [LayoutHorizontalComponent, RouterTestingModule], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(LayoutHorizontalComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/lib/components/layouts/layout-horizontal/layout-horizontal.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { FooterComponent, NavbarComponent } from '@lib/components'; 4 | 5 | @Component({ 6 | selector: 'app-layout-horizontal', 7 | standalone: true, 8 | imports: [CommonModule, NavbarComponent, FooterComponent], 9 | templateUrl: './layout-horizontal.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class LayoutHorizontalComponent {} 13 | -------------------------------------------------------------------------------- /src/app/lib/components/logo/logo.component.html: -------------------------------------------------------------------------------- 1 | 5 | 29 | AB 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/app/lib/components/logo/logo.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { FooterComponent } from '../footer/footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [FooterComponent, RouterTestingModule], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(FooterComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/lib/components/logo/logo.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-logo', 7 | standalone: true, 8 | imports: [CommonModule, RouterModule], 9 | templateUrl: './logo.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class LogoComponent {} 13 | -------------------------------------------------------------------------------- /src/app/lib/components/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/app/lib/components/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { NavbarComponent } from './navbar.component'; 5 | 6 | describe('NavbarComponent', () => { 7 | let component: NavbarComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [NavbarComponent, RouterTestingModule], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(NavbarComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/lib/components/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 3 | import { Router, RouterModule } from '@angular/router'; 4 | import { AuthService } from '@lib/services'; 5 | import { LogoComponent } from '../logo/logo.component'; 6 | 7 | @Component({ 8 | selector: 'app-navbar', 9 | standalone: true, 10 | imports: [CommonModule, RouterModule, LogoComponent], 11 | templateUrl: './navbar.component.html', 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | }) 14 | export class NavbarComponent { 15 | private readonly _router = inject(Router); 16 | private readonly _authService = inject(AuthService); 17 | 18 | onClickSignOut(): void { 19 | this._authService.logout(); 20 | this._router.navigate(['/auth/login']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { AppTheme } from './services/theme'; 2 | 3 | export const DEFAULT_BASE_THEME: AppTheme = 'system' as const; 4 | -------------------------------------------------------------------------------- /src/app/lib/enums/indext.ts: -------------------------------------------------------------------------------- 1 | export * from './user-role.enum'; 2 | -------------------------------------------------------------------------------- /src/app/lib/enums/user-role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum UserRole { 2 | Admin = 'ADMIN', 3 | User = 'USER', 4 | Guest = 'GUEST', 5 | } 6 | -------------------------------------------------------------------------------- /src/app/lib/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router'; 3 | import { AuthService } from '@lib/services'; 4 | import { authGuard } from './auth.guard'; 5 | 6 | describe('authGuard', () => { 7 | let authService: AuthService; 8 | let router: Router; 9 | let canMatch: CanMatchFn; 10 | 11 | beforeEach(() => { 12 | authService = TestBed.inject(AuthService); 13 | router = TestBed.inject(Router); 14 | canMatch = authGuard(); 15 | }); 16 | 17 | it('should allow navigation to proceed when requiresAuthentication is true and the user is logged in', () => { 18 | authService.isAuthenticated$.next(true); 19 | expect(canMatch({} as Route, [] as UrlSegment[])).toBe(true); 20 | }); 21 | 22 | it(`should block navigation and redirect to '/auth/login' when requiresAuthentication is true and the user is not logged in`, () => { 23 | authService.isAuthenticated$.next(false); 24 | expect(canMatch({} as Route, [] as UrlSegment[])).toEqual(router.createUrlTree(['/auth/login'])); 25 | }); 26 | 27 | it(`should block navigation and redirect to '/' when requiresAuthentication is false and the user is logged in`, () => { 28 | authService.isAuthenticated$.next(true); 29 | canMatch = authGuard({ requiresAuthentication: false }); 30 | expect(canMatch({} as Route, [] as UrlSegment[])).toEqual(router.createUrlTree(['/'])); 31 | }); 32 | 33 | it('should allow navigation to proceed when requiresAuthentication is false and the user is not logged in', () => { 34 | authService.isAuthenticated$.next(false); 35 | canMatch = authGuard({ requiresAuthentication: false }); 36 | expect(canMatch({} as Route, [] as UrlSegment[])).toBe(true); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/app/lib/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { CanMatchFn, Route, Router, UrlSegment } from '@angular/router'; 3 | import { AuthService } from '@lib/services'; 4 | 5 | type AuthGuardOptions = { 6 | requiresAuthentication: boolean; 7 | }; 8 | 9 | const defaultAuthGuardOptions = (): AuthGuardOptions => ({ 10 | requiresAuthentication: true, 11 | }); 12 | 13 | /** 14 | * Guard that allows or blocks the navigation based on the user's authentication status. 15 | * 16 | * @param options An optional object that configures the behavior of the guard. 17 | * @param options.requiresAuthentication An optional boolean that specifies whether the guard should allow or block navigation based on the user's authentication status. 18 | * 19 | * @returns A function that acts as an Angular route guard. 20 | * 21 | * @example 22 | * import { authGuard } from '@lib/guards'; 23 | * 24 | * const routes: Routes = [ 25 | * { 26 | * path: 'my-protected-component-path', 27 | * loadComponent: async () => (await import('./path/to/my-protected-component')).MyProtectedComponent, 28 | * canMatch: [authGuard()], 29 | * }, 30 | * ]; 31 | * 32 | * @example 33 | * import { authGuard } from '@lib/guards'; 34 | * 35 | * const routes: Routes = [ 36 | * { 37 | * path: 'my-path-component', 38 | * loadComponent: async () => (await import('./path/to/my-auth-component')).MyAuthComponent, 39 | * canMatch: [authGuard({ requiresAuthentication: false })], 40 | * }, 41 | * ]; 42 | */ 43 | export const authGuard = (options: AuthGuardOptions = defaultAuthGuardOptions()): CanMatchFn => { 44 | return (_: Route, segments: UrlSegment[]) => { 45 | const router = inject(Router); 46 | const authService = inject(AuthService); 47 | 48 | if (options.requiresAuthentication === authService.isAuthenticated) { 49 | return true; 50 | } 51 | 52 | return options.requiresAuthentication 53 | ? router.createUrlTree(['/auth/login'], { 54 | queryParams: { 55 | returnUrl: segments.map((s) => s.path).join('/'), 56 | }, 57 | }) 58 | : router.createUrlTree(['/']); 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/app/lib/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | -------------------------------------------------------------------------------- /src/app/lib/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.interceptor'; 2 | export * from './server-error.interceptor'; 3 | -------------------------------------------------------------------------------- /src/app/lib/interceptors/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpInterceptorFn } from '@angular/common/http'; 2 | import { inject } from '@angular/core'; 3 | import { environment } from '@env/environment'; 4 | import { AuthService } from '@lib/services'; 5 | 6 | /** 7 | * Interceptor that adds an Authorization header to requests that are authenticated and target the API URL. 8 | * 9 | * @param request The request object. 10 | * @param next The next interceptor in the chain. 11 | * 12 | * @returns The next Observable. 13 | */ 14 | export const jwtInterceptor: HttpInterceptorFn = (request, next) => { 15 | const authService = inject(AuthService); 16 | 17 | const isRequestAuthorized = authService.isAuthenticated && request.url.startsWith(environment.apiUrl); 18 | 19 | if (isRequestAuthorized) { 20 | const clonedRequest = request.clone({ 21 | setHeaders: { 22 | // eslint-disable-next-line @typescript-eslint/naming-convention 23 | Authorization: `Bearer ${'JWT TOKEN'}`, 24 | }, 25 | }); 26 | 27 | return next(clonedRequest); 28 | } 29 | 30 | return next(request); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/lib/interceptors/server-error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse, HttpInterceptorFn, HttpStatusCode } from '@angular/common/http'; 2 | import { inject } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { AuthService } from '@lib/services'; 5 | import { throwError } from 'rxjs'; 6 | import { catchError } from 'rxjs/operators'; 7 | 8 | /** 9 | * Interceptor that handles server errors. 10 | * 11 | * @param request The request object. 12 | * @param next The next interceptor in the chain. 13 | * 14 | * @returns The next Observable. 15 | */ 16 | export const serverErrorInterceptor: HttpInterceptorFn = (request, next) => { 17 | const router = inject(Router); 18 | const authService = inject(AuthService); 19 | 20 | return next(request).pipe( 21 | catchError((error: HttpErrorResponse) => { 22 | if ([HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden].includes(error.status)) { 23 | authService.logout(); 24 | router.navigateByUrl('/auth/login'); 25 | } 26 | 27 | return throwError(() => error); 28 | }), 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.interface'; 2 | -------------------------------------------------------------------------------- /src/app/lib/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | username: string; 3 | email: string; 4 | role: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/lib/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './package-json.token'; 2 | -------------------------------------------------------------------------------- /src/app/lib/providers/package-json.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, Provider } from '@angular/core'; 2 | import { name, version } from '../../../../package.json'; 3 | 4 | type PackageJson = { 5 | name: string; 6 | version: string; 7 | }; 8 | 9 | export const PACKAGE_JSON = new InjectionToken('PACKAGE_JSON'); 10 | 11 | export const providePackageJson = (): Provider => ({ 12 | provide: PACKAGE_JSON, 13 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 14 | useValue: { name, version } as PackageJson, 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/lib/services/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ teardown: { destroyAfterEach: false } }); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/lib/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { storage } from '@lib/utils/storage/storage.utils'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class AuthService { 9 | isAuthenticated$ = new BehaviorSubject(!!storage.getItem('appSession')); 10 | 11 | get isAuthenticated(): boolean { 12 | return this.isAuthenticated$.getValue(); 13 | } 14 | 15 | login(): void { 16 | storage.setItem('appSession', { user: 'some-user-id', token: 'abc' }); 17 | this.isAuthenticated$.next(true); 18 | } 19 | 20 | logout(): void { 21 | storage.removeItem('appSession'); 22 | this.isAuthenticated$.next(false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth/auth.service'; 2 | export * from './theme/theme.service'; 3 | -------------------------------------------------------------------------------- /src/app/lib/services/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme.config'; 2 | export * from './theme.service'; 3 | -------------------------------------------------------------------------------- /src/app/lib/services/theme/theme.config.ts: -------------------------------------------------------------------------------- 1 | export type AppTheme = 'system' | 'light' | 'dark'; 2 | -------------------------------------------------------------------------------- /src/app/lib/services/theme/theme.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { ThemeService } from './theme.service'; 4 | 5 | describe('ThemeService', () => { 6 | let service: ThemeService; 7 | let document: Document; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ teardown: { destroyAfterEach: false } }); 11 | service = TestBed.inject(ThemeService); 12 | document = TestBed.inject(DOCUMENT); 13 | }); 14 | 15 | it('should be created', () => { 16 | expect(service).toBeTruthy(); 17 | }); 18 | 19 | it('should set system theme as current theme', () => { 20 | service.setTheme('system'); 21 | expect(service.currentTheme).toBe('system'); 22 | }); 23 | 24 | it('should set system theme as a document.body class', () => { 25 | service.setTheme('system'); 26 | const bodyClasses = document.body.classList; 27 | expect(bodyClasses.contains(service.systemTheme)).toBeTruthy(); 28 | }); 29 | 30 | it('should set light theme as current theme', () => { 31 | service.setTheme('light'); 32 | expect(service.currentTheme).toBe('light'); 33 | }); 34 | 35 | it('should set light theme as a document.body class', () => { 36 | service.setTheme('light'); 37 | const bodyClasses = document.body.classList; 38 | expect(bodyClasses.contains('light')).toBeTruthy(); 39 | }); 40 | 41 | it('should set dark theme as current theme', () => { 42 | service.setTheme('dark'); 43 | expect(service.currentTheme).toBe('dark'); 44 | }); 45 | 46 | it('should set dark theme as a document.body class', () => { 47 | service.setTheme('dark'); 48 | const bodyClasses = document.body.classList; 49 | expect(bodyClasses.contains('dark')).toBeTruthy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/lib/services/theme/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Injectable, OnDestroy, inject } from '@angular/core'; 3 | import { DEFAULT_BASE_THEME } from '@lib/constants'; 4 | import { storage } from '@lib/utils'; 5 | import { BehaviorSubject, Subject, fromEventPattern } from 'rxjs'; 6 | import { takeUntil } from 'rxjs/operators'; 7 | import { AppTheme } from './theme.config'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ThemeService implements OnDestroy { 13 | currentTheme$ = new BehaviorSubject(this._storedTheme); 14 | 15 | private readonly _document = inject(DOCUMENT); 16 | 17 | private readonly _destroy$ = new Subject(); 18 | 19 | private readonly _mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 20 | 21 | public get currentTheme(): AppTheme | null { 22 | return this.currentTheme$.getValue(); 23 | } 24 | 25 | public get systemTheme(): AppTheme { 26 | return this._mediaQuery.matches ? 'dark' : 'light'; 27 | } 28 | 29 | private get _storedTheme(): AppTheme | null { 30 | return storage.getItem('appTheme'); 31 | } 32 | 33 | private set _storedTheme(theme: AppTheme | null) { 34 | storage.setItem('appTheme', theme as AppTheme); 35 | } 36 | 37 | ngOnDestroy(): void { 38 | this._destroy$.complete(); 39 | this._destroy$.unsubscribe(); 40 | } 41 | 42 | init(): void { 43 | this.setTheme(this._storedTheme || DEFAULT_BASE_THEME); 44 | this._listenForMediaQueryChanges(); 45 | } 46 | 47 | /** 48 | * Manually changes theme in LocalStorage & HTML body 49 | * 50 | * @param theme new theme 51 | */ 52 | setTheme(theme: AppTheme): void { 53 | this._clearThemes(); 54 | this._storedTheme = theme; 55 | 56 | let bodyClass = theme; 57 | this.currentTheme$.next(bodyClass); 58 | 59 | if (theme === 'system') { 60 | bodyClass = this.systemTheme; 61 | } 62 | this._document.body.classList.add(bodyClass); 63 | } 64 | 65 | /** 66 | * Handles system theme changes & applies theme automatically 67 | * 68 | */ 69 | private _listenForMediaQueryChanges(): void { 70 | fromEventPattern( 71 | this._mediaQuery.addListener.bind(this._mediaQuery), 72 | this._mediaQuery.removeListener.bind(this._mediaQuery), 73 | ) 74 | .pipe(takeUntil(this._destroy$)) 75 | .subscribe(() => { 76 | // Only applies changes when the current theme is "system" 77 | if (this._storedTheme === 'system') { 78 | this.setTheme('system'); 79 | } 80 | }); 81 | } 82 | 83 | /** 84 | * Clears all themes in ThemeList enum from the HTML element 85 | * 86 | */ 87 | private _clearThemes(): void { 88 | this._document.body.classList.remove('system', 'light', 'dark'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storage/storage.utils'; 2 | -------------------------------------------------------------------------------- /src/app/lib/utils/storage/storage.types.ts: -------------------------------------------------------------------------------- 1 | import { AppTheme } from '@lib/services/theme'; 2 | 3 | type StorageObjectMap = { 4 | appSession: { 5 | user: string; 6 | token: string; 7 | }; 8 | appTheme: AppTheme; 9 | }; 10 | 11 | export type StorageObjectType = 'appSession' | 'appTheme'; 12 | 13 | export type StorageObjectData = { 14 | type: T; 15 | data: StorageObjectMap[T]; 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/lib/utils/storage/storage.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { storage } from './storage.utils'; 2 | 3 | describe('StorageUtils', () => { 4 | it('should store & retrieve item from local & session storage', () => { 5 | // LocalStorage 6 | storage.setItem('App/theme', 'dark'); 7 | expect(storage.getItem('App/theme')).toBe('dark'); 8 | 9 | // sessionStorage 10 | storage.setItem('App/theme', 'dark', { api: 'SessionStorage' }); 11 | expect(storage.getItem('App/theme', { api: 'SessionStorage' })).toBe('dark'); 12 | }); 13 | 14 | it('should remove item from local & session storage', () => { 15 | // LocalStorage 16 | storage.setItem('App/theme', 'dark'); 17 | expect(storage.getItem('App/theme')).toBe('dark'); 18 | storage.removeItem('App/theme'); 19 | expect(storage.getItem('App/theme')).toBeNull(); 20 | 21 | // sessionStorage 22 | storage.setItem('App/theme', 'dark', { api: 'SessionStorage' }); 23 | expect(storage.getItem('App/theme', { api: 'SessionStorage' })).toBe('dark'); 24 | storage.removeItem('App/theme', { api: 'SessionStorage' }); 25 | expect(storage.getItem('App/theme', { api: 'SessionStorage' })).toBeNull(); 26 | }); 27 | 28 | it('should clear all items from local & session storage', () => { 29 | // LocalStorage 30 | storage.clear(); 31 | expect(localStorage.length).toBe(0); 32 | 33 | // sessionStorage 34 | storage.clear({ api: 'SessionStorage' }); 35 | expect(localStorage.length).toBe(0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/lib/utils/storage/storage.utils.ts: -------------------------------------------------------------------------------- 1 | import { StorageObjectData, StorageObjectType } from './storage.types'; 2 | 3 | type StorageOptions = { 4 | api?: 'LocalStorage' | 'SessionStorage'; 5 | }; 6 | 7 | function getStorageApi(api: StorageOptions['api']): Storage { 8 | return api === 'SessionStorage' ? sessionStorage : localStorage; 9 | } 10 | 11 | function getItem(item: T, options?: StorageOptions): StorageObjectData['data'] | null { 12 | const api = getStorageApi(options?.api || 'LocalStorage'); 13 | const data = api.getItem(item.toString()); 14 | return data ? (JSON.parse(data) as StorageObjectData['data']) : null; 15 | } 16 | 17 | function setItem( 18 | itemName: T, 19 | data: StorageObjectData['data'], 20 | options?: StorageOptions, 21 | ): void { 22 | if (data === null || data === undefined) { 23 | return; 24 | } 25 | 26 | const api = getStorageApi(options?.api || 'LocalStorage'); 27 | api.setItem(itemName, JSON.stringify(data)); 28 | } 29 | 30 | function removeItem(item: T, options?: StorageOptions): void { 31 | const api = getStorageApi(options?.api || 'LocalStorage'); 32 | api.removeItem(item); 33 | } 34 | 35 | function clear(options?: StorageOptions): void { 36 | const api = getStorageApi(options?.api || 'LocalStorage'); 37 | api.clear(); 38 | } 39 | 40 | export const storage = { 41 | getItem, 42 | setItem, 43 | removeItem, 44 | clear, 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/pages/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: 'login', 6 | title: 'Login', 7 | loadComponent: async () => (await import('./login/login.component')).LoginComponent, 8 | }, 9 | { 10 | path: 'register', 11 | title: 'Register', 12 | loadComponent: async () => (await import('./register/register.component')).RegisterComponent, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/app/pages/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |

You're not logged in!

3 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/pages/auth/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { LoginComponent } from './login.component'; 5 | 6 | describe('LoginComponent', () => { 7 | let component: LoginComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [LoginComponent, RouterTestingModule], 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(LoginComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/pages/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, inject } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { AuthService } from '@lib/services'; 5 | 6 | @Component({ 7 | standalone: true, 8 | imports: [CommonModule], 9 | templateUrl: './login.component.html', 10 | }) 11 | export class LoginComponent { 12 | @Input() returnUrl!: string; 13 | 14 | private readonly _router = inject(Router); 15 | private readonly _authService = inject(AuthService); 16 | 17 | login(): void { 18 | this._authService.login(); 19 | 20 | this._router.navigate([this.returnUrl ?? `/`]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/auth/register/register.component.html: -------------------------------------------------------------------------------- 1 |

register works!

2 | -------------------------------------------------------------------------------- /src/app/pages/auth/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [RegisterComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(RegisterComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/pages/auth/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [CommonModule], 7 | templateUrl: './register.component.html', 8 | }) 9 | export class RegisterComponent {} 10 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Home

3 | 4 |

Choose your theme

5 | 6 |
7 | 15 | 24 | 33 |
34 | 35 |
36 |

Settings

37 | 63 |
64 | 65 |
66 |

Profiles

67 | 96 |
97 |
98 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HomeComponent, RouterTestingModule], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(HomeComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, inject, OnDestroy, OnInit } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { AppTheme, ThemeService } from '@lib/services/theme'; 5 | import { Subject, takeUntil } from 'rxjs'; 6 | 7 | @Component({ 8 | standalone: true, 9 | imports: [CommonModule, RouterModule], 10 | templateUrl: './home.component.html', 11 | }) 12 | export class HomeComponent implements OnInit, OnDestroy { 13 | currentTheme!: AppTheme | null; 14 | 15 | private readonly _themeService = inject(ThemeService); 16 | 17 | private readonly _destroy$ = new Subject(); 18 | 19 | ngOnInit(): void { 20 | this._themeService.currentTheme$ 21 | .pipe(takeUntil(this._destroy$)) 22 | .subscribe((theme) => (this.currentTheme = theme)); 23 | } 24 | 25 | ngOnDestroy(): void { 26 | this._destroy$.complete(); 27 | this._destroy$.unsubscribe(); 28 | } 29 | 30 | handleThemeChange(theme: AppTheme): void { 31 | this._themeService.setTheme(theme); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/pages/home/index.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | title: 'Home', 7 | loadComponent: async () => (await import('./home.component')).HomeComponent, 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /src/app/pages/screens/index.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '**', 6 | loadComponent: async () => (await import('@pages/screens/not-found/not-found.component')).NotFoundComponent, 7 | }, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/pages/screens/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |
5 |
8 | 17 | 33 | 37 | 38 | 41 | 42 | 43 | 44 |
45 |

The page you were looking for was not found

46 | 50 | Go home 51 | 52 |
53 |
54 | -------------------------------------------------------------------------------- /src/app/pages/screens/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { NotFoundComponent } from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [NotFoundComponent, RouterTestingModule], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(NotFoundComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/pages/screens/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | @Component({ 6 | standalone: true, 7 | imports: [CommonModule, RouterModule], 8 | templateUrl: './not-found.component.html', 9 | }) 10 | export class NotFoundComponent {} 11 | -------------------------------------------------------------------------------- /src/app/pages/settings/accessibility/accessibility.component.html: -------------------------------------------------------------------------------- 1 | Accessibility works! 2 | -------------------------------------------------------------------------------- /src/app/pages/settings/accessibility/accessibility.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { AccessibilityComponent } from './accessibility.component'; 3 | 4 | describe('AccessibilityComponent', () => { 5 | let component: AccessibilityComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(async () => { 9 | await TestBed.configureTestingModule({ 10 | imports: [AccessibilityComponent], 11 | }).compileComponents(); 12 | 13 | fixture = TestBed.createComponent(AccessibilityComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/pages/settings/accessibility/accessibility.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [CommonModule], 7 | templateUrl: './accessibility.component.html', 8 | }) 9 | export class AccessibilityComponent {} 10 | -------------------------------------------------------------------------------- /src/app/pages/settings/account/account.component.html: -------------------------------------------------------------------------------- 1 |

account works!

2 | -------------------------------------------------------------------------------- /src/app/pages/settings/account/account.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { AccountComponent } from './account.component'; 3 | 4 | describe('AccountComponent', () => { 5 | let component: AccountComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(async () => { 9 | await TestBed.configureTestingModule({ 10 | imports: [AccountComponent], 11 | }).compileComponents(); 12 | 13 | fixture = TestBed.createComponent(AccountComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/pages/settings/account/account.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [CommonModule], 7 | templateUrl: './account.component.html', 8 | }) 9 | export class AccountComponent {} 10 | -------------------------------------------------------------------------------- /src/app/pages/settings/appearance/appearance.component.html: -------------------------------------------------------------------------------- 1 |

appearance works!

2 | -------------------------------------------------------------------------------- /src/app/pages/settings/appearance/appearance.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { AppearanceComponent } from './appearance.component'; 3 | 4 | describe('AppearanceComponent', () => { 5 | let component: AppearanceComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(async () => { 9 | await TestBed.configureTestingModule({ 10 | imports: [AppearanceComponent], 11 | }).compileComponents(); 12 | 13 | fixture = TestBed.createComponent(AppearanceComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/pages/settings/appearance/appearance.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [CommonModule], 7 | templateUrl: './appearance.component.html', 8 | }) 9 | export class AppearanceComponent {} 10 | -------------------------------------------------------------------------------- /src/app/pages/settings/index.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: 'accessibility', 6 | title: 'Accessibility settings', 7 | loadComponent: async () => (await import('./accessibility/accessibility.component')).AccessibilityComponent, 8 | }, 9 | { 10 | path: 'account', 11 | title: 'Account settings', 12 | loadComponent: async () => (await import('./account/account.component')).AccountComponent, 13 | }, 14 | { 15 | path: 'appearance', 16 | title: 'Appearance settings', 17 | loadComponent: async () => (await import('./appearance/appearance.component')).AppearanceComponent, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/pages/user/index.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | title: 'Profile', 7 | loadComponent: async () => (await import('./profile/profile.component')).ProfileComponent, 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /src/app/pages/user/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |

User: {{ username }}

2 | -------------------------------------------------------------------------------- /src/app/pages/user/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { ProfileComponent } from './profile.component'; 4 | 5 | describe('ProfileComponent', () => { 6 | let component: ProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ProfileComponent, RouterTestingModule], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(ProfileComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/pages/user/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [CommonModule], 7 | templateUrl: './profile.component.html', 8 | }) 9 | export class ProfileComponent { 10 | @Input() username!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ju4n97/angular-boilerplate/66d9b6a9805c93acf745602a75cb77ebb98bbe72/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | apiUrl: 'http://localhost:3000/api/v1', 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: true, 7 | apiUrl: 'http://localhost:3000/api/v1', 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ju4n97/angular-boilerplate/66d9b6a9805c93acf745602a75cb77ebb98bbe72/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Boilerplate 6 | 7 | 11 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | import { appConfig } from './app/app.config'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((error) => { 6 | console.error(error); 7 | }); 8 | -------------------------------------------------------------------------------- /src/theme/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ju4n97/angular-boilerplate/66d9b6a9805c93acf745602a75cb77ebb98bbe72/src/theme/.gitkeep -------------------------------------------------------------------------------- /src/theme/01-base/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ju4n97/angular-boilerplate/66d9b6a9805c93acf745602a75cb77ebb98bbe72/src/theme/01-base/.gitkeep -------------------------------------------------------------------------------- /src/theme/01-base/document.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | @apply h-full; 4 | } 5 | 6 | body { 7 | @apply bg-slate-100; 8 | @apply text-slate-900; 9 | } 10 | 11 | body.dark { 12 | @apply bg-zinc-900; 13 | @apply text-zinc-200; 14 | } 15 | -------------------------------------------------------------------------------- /src/theme/01-base/font.css: -------------------------------------------------------------------------------- 1 | html { 2 | @apply font-body; 3 | @apply font-light; 4 | } 5 | -------------------------------------------------------------------------------- /src/theme/01-base/heading.css: -------------------------------------------------------------------------------- 1 | h1, 2 | .h1, 3 | h2, 4 | .h2, 5 | h3, 6 | .h3, 7 | h4, 8 | .h4, 9 | h5, 10 | .h5, 11 | h6, 12 | .h6, 13 | .display-1, 14 | .display-2, 15 | .display-3, 16 | .display-4 { 17 | @apply font-display; 18 | @apply mt-0; 19 | } 20 | 21 | h1, 22 | .h1 { 23 | @apply text-4xl; 24 | @apply mb-12; 25 | } 26 | 27 | h2, 28 | .h2 { 29 | @apply text-3xl; 30 | @apply mb-10; 31 | } 32 | 33 | h3, 34 | .h3 { 35 | @apply text-2xl; 36 | @apply mb-8; 37 | } 38 | 39 | h4, 40 | .h4 { 41 | @apply text-xl; 42 | @apply mb-6; 43 | } 44 | 45 | h5, 46 | .h5 { 47 | @apply text-lg; 48 | @apply mb-4; 49 | } 50 | 51 | h6, 52 | .h6 { 53 | @apply text-base; 54 | @apply mb-2; 55 | } 56 | 57 | .display-1 { 58 | @apply text-8xl; 59 | @apply mb-32; 60 | } 61 | 62 | .display-2 { 63 | @apply text-7xl; 64 | @apply mb-24; 65 | } 66 | 67 | .display-3 { 68 | @apply text-6xl; 69 | @apply mb-20; 70 | } 71 | 72 | .display-4 { 73 | @apply text-5xl; 74 | @apply mb-16; 75 | } 76 | -------------------------------------------------------------------------------- /src/theme/01-base/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --size-navbar-height: 60px; 3 | --size-footer-height: 180px; 4 | } 5 | -------------------------------------------------------------------------------- /src/theme/02-components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ju4n97/angular-boilerplate/66d9b6a9805c93acf745602a75cb77ebb98bbe72/src/theme/02-components/.gitkeep -------------------------------------------------------------------------------- /src/theme/02-components/button.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply w-full md:w-auto; 3 | @apply border border-transparent rounded-lg outline-none; 4 | @apply h-10 min-w-[80px] py-2 px-3; 5 | @apply relative inline-flex items-center justify-center; 6 | @apply text-sm text-center font-medium leading-none; 7 | @apply select-none cursor-pointer; 8 | } 9 | 10 | .btn:disabled { 11 | @apply opacity-80 cursor-not-allowed; 12 | } 13 | 14 | .btn:not(:disabled):hover { 15 | @apply bg-opacity-80; 16 | } 17 | 18 | .btn:not(:disabled):focus { 19 | @apply ring-2 ring-gray-400 ring-offset-2 dark:ring-zinc-700; 20 | } 21 | 22 | .btn-fit { 23 | @apply min-w-fit; 24 | } 25 | 26 | .btn-secondary { 27 | @apply bg-zinc-200 text-zinc-900 dark:bg-zinc-700 dark:text-white; 28 | } 29 | 30 | .btn-primary { 31 | @apply bg-blue-500 text-white; 32 | } 33 | -------------------------------------------------------------------------------- /src/theme/02-components/link.css: -------------------------------------------------------------------------------- 1 | a.link { 2 | @apply font-semibold; 3 | @apply text-indigo-500 dark:text-indigo-300; 4 | } 5 | 6 | a.link:hover { 7 | @apply underline; 8 | @apply text-indigo-400; 9 | } 10 | 11 | a.link:focus { 12 | @apply text-indigo-800 dark:text-indigo-400; 13 | } 14 | 15 | a.link:visited { 16 | @apply text-rose-500 dark:text-rose-400; 17 | } 18 | -------------------------------------------------------------------------------- /src/theme/03-utilities/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ju4n97/angular-boilerplate/66d9b6a9805c93acf745602a75cb77ebb98bbe72/src/theme/03-utilities/.gitkeep -------------------------------------------------------------------------------- /src/theme/03-utilities/border.css: -------------------------------------------------------------------------------- 1 | .border-base { 2 | @apply border-zinc-200 dark:border-zinc-800; 3 | } 4 | -------------------------------------------------------------------------------- /src/theme/styles.css: -------------------------------------------------------------------------------- 1 | /* base */ 2 | @import './tailwindcss/base.css'; 3 | @import './01-base/variables.css'; 4 | @import './01-base/document.css'; 5 | @import './01-base/font.css'; 6 | @import './01-base/heading.css'; 7 | 8 | /* components */ 9 | @import './tailwindcss/components.css'; 10 | @import './02-components/link.css'; 11 | @import './02-components/button.css'; 12 | 13 | /* utilities */ 14 | @import './tailwindcss/utilities.css'; 15 | @import './03-utilities/border.css'; 16 | -------------------------------------------------------------------------------- /src/theme/tailwindcss/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | -------------------------------------------------------------------------------- /src/theme/tailwindcss/components.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | -------------------------------------------------------------------------------- /src/theme/tailwindcss/utilities.css: -------------------------------------------------------------------------------- 1 | @tailwind utilities; 2 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import { addDynamicIconSelectors } from '@iconify/tailwind'; 3 | 4 | const TAILWIND_PLUGINS = [ 5 | require('@tailwindcss/aspect-ratio'), 6 | require('@tailwindcss/forms'), 7 | require('@tailwindcss/typography'), 8 | ]; 9 | 10 | const CUSTOM_PLUGINS = [addDynamicIconSelectors()]; 11 | 12 | /** @type {import('tailwindcss').Config} */ 13 | export default { 14 | content: ['./src/**/*.{html,ts}', './projects/**/*.{html,ts}'], 15 | darkMode: 'class', 16 | theme: { 17 | fontFamily: { 18 | display: ['Oswald', 'sans-serif'], 19 | body: ['Poppins', 'sans-serif'], 20 | }, 21 | container: { 22 | center: true, 23 | padding: '1.5rem', 24 | }, 25 | extend: {}, 26 | }, 27 | plugins: [...TAILWIND_PLUGINS, ...CUSTOM_PLUGINS], 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "resolveJsonModule": true, 20 | "allowSyntheticDefaultImports": true, 21 | "target": "ES2022", 22 | "module": "ES2022", 23 | "useDefineForClassFields": false, 24 | "lib": ["ES2022", "dom"], 25 | "paths": { 26 | "@lib/*": ["src/app/lib/*"], 27 | "@pages/*": ["src/app/pages/*"], 28 | "@env/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | --------------------------------------------------------------------------------