├── .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 |
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 | [](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 | [](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 | [](https://vercel.com/new/clone?repository-url=https://github.com/juanmesa2097/angular-boilerplate)
115 |
116 | ## 📦 Despliegue en Netlify
117 |
118 | [](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 |
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 | [](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 | [](https://vercel.com/new/clone?repository-url=https://github.com/juanmesa2097/angular-boilerplate)
115 |
116 | ## 📦 Deploy to Netlify
117 |
118 | [](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 | Angular Boilerplate
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 |
64 |
65 |
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 |
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 |
--------------------------------------------------------------------------------