├── .all-contributorsrc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ ├── FEATURE_REQUEST.yml │ └── config.yml ├── MAINTENANCE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── chromatic.yml │ ├── continuous-integration.yml │ ├── playwright.yml │ └── stylelint.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .stylelintrc.json ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular.json ├── commitlint.config.js ├── e2e └── example.spec.ts ├── eslint.config.js ├── netlify.toml ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── favicon.png ├── favicon.svg ├── icons │ ├── add-icon.svg │ ├── error-icon.svg │ └── valid-icon.svg ├── light-logo.gif └── light-logo.svg ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ ├── app.routes.ts │ ├── core │ │ ├── auth │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── login │ │ │ │ ├── login.component.css │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.spec.ts │ │ │ │ └── login.component.ts │ │ │ ├── models │ │ │ │ ├── login-payload.model.ts │ │ │ │ └── signup-payload.model.ts │ │ │ └── signup │ │ │ │ ├── signup.component.css │ │ │ │ ├── signup.component.html │ │ │ │ ├── signup.component.spec.ts │ │ │ │ └── signup.component.ts │ │ └── layout │ │ │ └── header │ │ │ ├── header.component.css │ │ │ ├── header.component.html │ │ │ ├── header.component.spec.ts │ │ │ └── header.component.ts │ ├── features │ │ ├── about │ │ │ ├── about.routes.ts │ │ │ └── pages │ │ │ │ └── about │ │ │ │ ├── about.component.css │ │ │ │ ├── about.component.html │ │ │ │ ├── about.component.spec.ts │ │ │ │ └── about.component.ts │ │ ├── communities │ │ │ ├── communities.routes.ts │ │ │ └── pages │ │ │ │ └── community-list │ │ │ │ ├── community-list.component.css │ │ │ │ ├── community-list.component.html │ │ │ │ ├── community-list.component.spec.ts │ │ │ │ └── community-list.component.ts │ │ └── events │ │ │ ├── events.routes.ts │ │ │ └── pages │ │ │ └── event-list │ │ │ ├── event-list.component.css │ │ │ ├── event-list.component.html │ │ │ ├── event-list.component.spec.ts │ │ │ └── event-list.component.ts │ └── shared │ │ ├── components │ │ ├── form-field │ │ │ ├── form-field.component.css │ │ │ ├── form-field.component.html │ │ │ ├── form-field.component.spec.ts │ │ │ └── form-field.component.ts │ │ └── vertical-nav │ │ │ ├── vertical-nav.component.css │ │ │ ├── vertical-nav.component.html │ │ │ ├── vertical-nav.component.spec.ts │ │ │ └── vertical-nav.component.ts │ │ ├── directives │ │ ├── error-message.directive.spec.ts │ │ ├── error-message.directive.ts │ │ ├── field.directive.spec.ts │ │ ├── field.directive.ts │ │ ├── form.directive.spec.ts │ │ ├── form.directive.ts │ │ ├── hint-message.directive.spec.ts │ │ └── hint-message.directive.ts │ │ └── models │ │ └── vertical-link.model.ts ├── environments │ ├── environment.development.ts │ └── environment.ts ├── index.html ├── main.ts ├── proxy.conf.json ├── styles.css └── styles │ ├── components │ ├── buttons.css │ ├── card.css │ ├── event.css │ ├── form.css │ ├── layout.css │ └── post.css │ ├── reset.css │ └── tokens │ ├── colors.css │ └── shapes.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "realworld-angular-starter", 3 | "projectOwner": "realworld-angular", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributorsPerLine": 7, 10 | "contributors": [ 11 | { 12 | "login": "geromegrignon", 13 | "name": "Gerome Grignon", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/32737308?v=4", 15 | "profile": "https://gerome.dev", 16 | "contributions": [ 17 | "maintenance" 18 | ] 19 | }, 20 | { 21 | "login": "ZvSimon", 22 | "name": "Simon", 23 | "avatar_url": "https://avatars.githubusercontent.com/u/73712759?v=4", 24 | "profile": "https://github.com/ZvSimon", 25 | "contributions": [ 26 | "code" 27 | ] 28 | }, 29 | { 30 | "login": "LucasDerhore", 31 | "name": "Ldh", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/91560378?v=4", 33 | "profile": "https://www.linkedin.com/in/lucas-derhore-591549150/", 34 | "contributions": [ 35 | "code" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report a bug in the RealWorld Angular starter project 3 | title: '[Bug]: ' 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of the problem 11 | validations: 12 | required: true 13 | - type: dropdown 14 | id: contribute 15 | attributes: 16 | label: Do you want to contribute with a pull request? 17 | options: 18 | - 'Yes' 19 | - 'No' 20 | validations: 21 | required: true 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest a feature for RealWorld Angular starter project 3 | title: '[Feature Request]:' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: '# Feature Request' 8 | - type: textarea 9 | attributes: 10 | label: Description 11 | description: ' ' 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Describe the solution you'd like 17 | description: If you have a solution in mind, please describe it. 18 | - type: textarea 19 | attributes: 20 | label: Describe alternatives you've considered 21 | description: Have you considered any alternative solutions or workarounds? 22 | - type: dropdown 23 | id: contribute 24 | attributes: 25 | label: Do you want to contribute with a pull request? 26 | options: 27 | - 'Yes' 28 | - 'No' 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/orgs/realworld-angular/discussions 5 | about: Have a question or want to discuss something? 6 | - name: Documentation and API repository 7 | url: https://github.com/realworld-angular/realworld-angular 8 | about: Documentation and API for RealWorld Angular 9 | -------------------------------------------------------------------------------- /.github/MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | > This file is aimed to maintainer onboarding 4 | 5 | ## Chromatic 6 | 7 | To run Chromatic locally, update the `CHROMATIC_PROJECT_TOKEN` in the `.env` file with the token from the Chromatic dashboard. 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | ## PR Checklist 8 | 9 | Please check if your PR fulfills the following requirements: 10 | 11 | - [ ] The commit message follows our guidelines: https://github.com/realworld-angular/realworld-angular-starter/blob/main/CONTRIBUTING.md 12 | - [ ] Tests for the changes have been added (for bug fixes / features) 13 | - [ ] Docs have been added / updated (for bug fixes / features) 14 | 15 | ## PR Type 16 | 17 | What kind of change does this PR introduce? 18 | 19 | 20 | 21 | - [ ] Bugfix 22 | - [ ] Feature 23 | - [ ] Code style update (formatting, local variables) 24 | - [ ] Refactoring (no functional changes, no api changes) 25 | - [ ] Build related changes 26 | - [ ] CI related changes 27 | - [ ] Documentation content changes 28 | - [ ] Other... Please describe: 29 | 30 | ## What is the current behavior? 31 | 32 | 33 | 34 | Issue Number: N/A 35 | 36 | ## What is the new behavior? 37 | 38 | ## Does this PR introduce a breaking change? 39 | 40 | - [ ] Yes 41 | - [ ] No 42 | 43 | 44 | 45 | ## Other information 46 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: pnpm 10 | directory: '/' 11 | schedule: 12 | interval: monthly 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | name: "Chromatic" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | chromatic: 10 | name: Run Chromatic 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 21 | 22 | - name: Install dependencies 23 | run: pnpm install --frozen-lockfile 24 | 25 | - name: Run Chromatic 26 | uses: chromaui/action@latest 27 | with: 28 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Format Check 2 | 3 | on: 4 | push: 5 | types: 6 | - pushed 7 | pull_request: 8 | 9 | jobs: 10 | format-check: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: pnpm/action-setup@v4 20 | with: 21 | version: 9 22 | 23 | - name: Install dependencies 24 | run: pnpm install --frozen-lockfile 25 | 26 | - name: Run Prettier check 27 | run: pnpm prettier --check "src/**/*.{ts,html,css,json,md,yaml,yml}" 28 | 29 | - name: Run ESLint check 30 | run: pnpm run lint 31 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | push: 5 | types: 6 | - pushed 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | timeout-minutes: 60 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: lts/* 18 | - name: Install dependencies 19 | run: npm install -g pnpm && pnpm install 20 | - name: Install Playwright Browsers 21 | run: pnpm exec playwright install --with-deps 22 | - name: Run Playwright tests 23 | run: pnpm exec playwright test 24 | - uses: actions/upload-artifact@v4 25 | if: always() 26 | with: 27 | name: playwright-report 28 | path: playwright-report/ 29 | retention-days: 30 30 | -------------------------------------------------------------------------------- /.github/workflows/stylelint.yml: -------------------------------------------------------------------------------- 1 | name: Lint CSS 2 | 3 | on: 4 | push: 5 | types: 6 | - pushed 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 21 | 22 | - name: Install dependencies 23 | run: pnpm install --frozen-lockfile 24 | 25 | - name: Run Stylelint 26 | run: pnpm stylelint "**/*.css" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-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 | 44 | .env 45 | /test-results/ 46 | /playwright-report/ 47 | /blob-report/ 48 | /playwright/.cache/ 49 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm dlx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | 4 | "rules": { 5 | "no-empty-source": null, 6 | "at-rule-no-unknown": null, 7 | "no-duplicate-selectors": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 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/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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Welcome to the contribution guide for the RealWorld Angular project! We are excited to have you here. This guide will help you get started with contributing to the project. We appreciate your help in making this project better. 4 | 5 | ## Code of Conduct 6 | 7 | Please read and follow our [Code of Conduct](https://github.com/realworld-angular/realworld-angular-starter?tab=coc-ov-file) to ensure a welcoming and inclusive environment. 8 | 9 | ## Contribution opportunities 10 | 11 | There are many ways to contribute to the project. Here are some ideas to get you started: 12 | 13 | - **Bug reports**: If you find a bug in the project, please report to the [Found a bug?](#found-a-bug) section. 14 | - **Feature requests**: If you have a feature request, please report to the [Missing a feature?](#missing-a-feature) section. 15 | - **Documentation**: If you find a typo, please open a pull request to fix it. If you want to improve the documentation, firstly open an issue to track the changes. 16 | - **Triaging issues**: If you have some spare time, you can help by triaging issues. This will help to keep the project organized and ensure that issues are addressed promptly. 17 | - **Review pull requests**: If you don't have time to contribute with code, you can help by reviewing pull requests. Your feedback is valuable to ensure the quality of the project. 18 | - **Spread the word**: If you like the project, please share it with your friends and colleagues. The more people know about the project, the more likely it is to grow. 19 | - **Donate**: If you want to support the project financially, you can [donate](https://buymeacoffee.com/geromegrignon) to the maintainers. This will help to cover the project's costs and ensure its sustainability. 20 | - **Feedback**: If you have any feedback about the project, please open a discussion in the [Discussions](https://github.com/orgs/realworld-angular/discussions). Your feedback is essential to improve the project. 21 | - **Other**: If you have any other idea to contribute to the project, please let us know. We are open to new ideas and suggestions. 22 | 23 | ## Got a question or problem? 24 | 25 | [GitHub issues](https://github.com/realworld-angular/realworld-angular-starter/issues) is the best place for bug reports and feature requests. 26 | If you have some other question not required to be tracked in the issues, please open a discussion in the [Discussions](https://github.com/orgs/realworld-angular-starter/discussions). 27 | 28 | ## Found a bug? 29 | 30 | If you find a bug in the project, please firstly check if it's tracked in the [GitHub issues](https://github.com/realworld-angular/realworld-angular-starter/issues). 31 | 32 | If there is already an issue for the bug, please add a comment to the existing issue if you have some additional information to help in solving it. 33 | If there is no issue for the bug, please create a new issue [here](https://github.com/realworld-angular/realworld-angular-starter/issues/new/choose). 34 | 35 | ### Submission Guidelines 36 | 37 | Fill the issue template with the required information. The more information you provide, the easier it will be to reproduce and fix the bug. 38 | Open-source communication is asynchronous by nature, so the more complete is each issue, the more likely it is to be addressed quickly. 39 | 40 | ## Missing a feature? 41 | 42 | If you have a feature request, please firstly check if it's tracked in the [GitHub issues](https://github.com/realworld-angular/realworld-angular-starter/issues). 43 | 44 | If there is already an issue for the bug, feel free to comment it with your thoughts. A fresh perspective is always welcome. 45 | If there is no issue for the feature, please create a new issue [here](https://github.com/realworld-angular/realworld-angular-starter/issues/new/choose). 46 | 47 | We encourage you to wait for feedback from the maintainers before starting to work on the feature. This will help to avoid duplicated work and ensure that the feature fits the project's goals. 48 | 49 | ### Submission Guidelines 50 | 51 | Fill the issue template with the required information. The more information you provide, the easier it will be to understand your request and evaluate its feasibility. 52 | Open-source communication is asynchronous by nature, so the more complete is each issue, the more likely it is to be addressed quickly. 53 | 54 | ## Contribute to an existing issue 55 | 56 | If you want to contribute to an existing issue, please check the issue's comments to see if someone is already working on it. If not, feel free to ask if you can help. 57 | Once assigned, you can start working on the issue. Please follow the guidelines provided by the maintainers and make sure to respect the project's coding standards. 58 | 59 | > If you are already assigned an issue, keep focused on it. If you want to work on another issue, please ask for reassignment. 60 | 61 | ### Get a copy of the project 62 | 63 | To contribute to the project, you need to fork the repository and clone it to your local machine. You can find more information on how to do it [here](https://docs.github.com/en/get-started/quickstart/fork-a-repo). 64 | 65 | ### Create a branch 66 | 67 | Before starting to work on a new feature or bug fix, create a new branch. This will help to keep your changes isolated from the main branch and make it easier to review your changes. 68 | 69 | ```bash 70 | git checkout -b feature/my-new-feature 71 | ``` 72 | 73 | ### Make your changes 74 | 75 | Once you have created a new branch, you can start making your changes. Make sure to follow the project's coding standards and guidelines. 76 | Ask for any details in the GitHub Issue to help you fully understand the issue you are working on. 77 | Feel free to commit your changes as you progress and to open a [draft pull request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) to get feedback from the maintainers. 78 | 79 | > Running out of time to contribute is perfectly fine. Just let us know in the issue comments, and we can assign the issue to someone else. 80 | 81 | ### Commit your changes 82 | 83 | After you have made your changes, commit them to your branch. Make sure to write a clear and concise commit message that explains the changes you have made. 84 | 85 | ```bash 86 | git commit -m "feat(api): add a new feature" 87 | ``` 88 | 89 | ### Commit message guidelines 90 | 91 | Use the following format with a type, a scope and a short summary: 92 | 93 | ``` 94 | (): 95 | │ │ │ 96 | │ │ └─⫸ Summary in present tense. Not capitalized. present tense and imperative mood ("Add feature" not "Added feature") 97 | │ │ 98 | │ └─⫸ Commit Scope: docs|api|api-testing 99 | │ 100 | └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test 101 | ``` 102 | 103 | 104 | ### Push your changes 105 | 106 | After you have committed your changes, push them to your fork on GitHub. This will make your changes available for a pull request. 107 | 108 | ```bash 109 | git push origin feature/my-new-feature 110 | ``` 111 | 112 | ### Open a pull request 113 | 114 | Once you have pushed your changes to your fork on GitHub, you can open a [pull request](https://github.com/realworld-angular/realworld-angular-starter/pulls). Make sure to fill the pull request template with the required information. 115 | 116 | ### Submission Guidelines 117 | 118 | Fill the pull request template with the required information. The more information you provide, the easier it will be to review your changes. 119 | Open-source communication is asynchronous by design, so the more complete is each pull request, the more likely it is to be reviewed quickly. 120 | 121 | ## License 122 | 123 | By contributing to this project, you agree that your contributions will be licensed under the same license as the project: Apache 2.0 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Realworld Angular Starter 2 | 3 | ![continuous integration](https://github.com/realworld-angular/realworld-angular-starter/actions/workflows/continuous-integration.yml/badge.svg) 4 | 5 | ## Development setup 6 | 7 | To run the documentation locally, you need to have [Node.js](https://nodejs.org/en) and [pnpm](https://pnpm.io/fr/) installed on your machine. 8 | 9 | ### Install dependencies 10 | 11 | ```bash 12 | pnpm install 13 | ``` 14 | 15 | ### Start the development server 16 | 17 | ```bash 18 | pnpm start 19 | ``` 20 | 21 | The application will be available at [http://localhost:4200](http://localhost:4200) by default. Check your terminal for the exact URL. 22 | 23 | ## Commands reference 24 | 25 | All commands are run from the root of the project, from a terminal: 26 | 27 | | Command | Action | 28 | |:------------------------------------|:-----------------------------------------------| 29 | | `pnpm install` | Installs dependencies | 30 | | `pnpm start` | Starts local dev server at `localhost:4200` | 31 | | `pnpm build` | Build your production site to `./dist/browser` | 32 | | `pnpm exec playwright test` | Run e2e tests | 33 | | `pnpm exec playwright test --ui` | Run e2e tests in interactive UI mode | 34 | | `pnpm exec playwright test --debug` | Runs the tests in debug mode. | 35 | | `pnpm exec playwright codegen` | Auto generate tests with Codegen | 36 | 37 | 38 | ## Contributors 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
Gerome Grignon
Gerome Grignon

🚧
Simon
Simon

💻
Ldh
Ldh

💻
52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "pnpm", 6 | "analytics": false, 7 | "schematicCollections": [ 8 | "@angular-eslint/schematics" 9 | ] 10 | }, 11 | "newProjectRoot": "projects", 12 | "projects": { 13 | "realworld-angular-starter": { 14 | "projectType": "application", 15 | "schematics": { 16 | "@schematics/angular:component": { 17 | "changeDetection": "OnPush" 18 | } 19 | }, 20 | "root": "", 21 | "sourceRoot": "src", 22 | "prefix": "app", 23 | "architect": { 24 | "build": { 25 | "builder": "@angular-devkit/build-angular:application", 26 | "options": { 27 | "outputPath": "dist/realworld-angular-starter", 28 | "index": "src/index.html", 29 | "browser": "src/main.ts", 30 | "polyfills": [ 31 | "zone.js" 32 | ], 33 | "tsConfig": "tsconfig.app.json", 34 | "assets": [ 35 | { 36 | "glob": "**/*", 37 | "input": "public" 38 | } 39 | ], 40 | "styles": [ 41 | "src/styles.css" 42 | ], 43 | "scripts": [] 44 | }, 45 | "configurations": { 46 | "production": { 47 | "budgets": [ 48 | { 49 | "type": "initial", 50 | "maximumWarning": "500kB", 51 | "maximumError": "1MB" 52 | }, 53 | { 54 | "type": "anyComponentStyle", 55 | "maximumWarning": "2kB", 56 | "maximumError": "4kB" 57 | } 58 | ], 59 | "outputHashing": "all" 60 | }, 61 | "development": { 62 | "optimization": false, 63 | "extractLicenses": false, 64 | "sourceMap": true, 65 | "fileReplacements": [ 66 | { 67 | "replace": "src/environments/environment.ts", 68 | "with": "src/environments/environment.development.ts" 69 | } 70 | ] 71 | } 72 | }, 73 | "defaultConfiguration": "production" 74 | }, 75 | "serve": { 76 | "builder": "@angular-devkit/build-angular:dev-server", 77 | "options": { 78 | "proxyConfig": "./src/proxy.conf.json" 79 | }, 80 | "configurations": { 81 | "production": { 82 | "buildTarget": "realworld-angular-starter:build:production" 83 | }, 84 | "development": { 85 | "buildTarget": "realworld-angular-starter:build:development" 86 | } 87 | }, 88 | "defaultConfiguration": "development" 89 | }, 90 | "extract-i18n": { 91 | "builder": "@angular-devkit/build-angular:extract-i18n" 92 | }, 93 | "test": { 94 | "builder": "@angular-devkit/build-angular:karma", 95 | "options": { 96 | "polyfills": [ 97 | "zone.js", 98 | "zone.js/testing" 99 | ], 100 | "tsConfig": "tsconfig.spec.json", 101 | "assets": [ 102 | { 103 | "glob": "**/*", 104 | "input": "public" 105 | } 106 | ], 107 | "styles": [ 108 | "src/styles.css" 109 | ], 110 | "scripts": [] 111 | } 112 | }, 113 | "lint": { 114 | "builder": "@angular-eslint/builder:lint", 115 | "options": { 116 | "lintFilePatterns": [ 117 | "src/**/*.ts", 118 | "src/**/*.html" 119 | ] 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /e2e/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('https://playwright.dev/'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/); 8 | }); 9 | 10 | test('get started link', async ({ page }) => { 11 | await page.goto('https://playwright.dev/'); 12 | 13 | // Click the get started link. 14 | await page.getByRole('link', { name: 'Get started' }).click(); 15 | 16 | // Expects page to have a heading with the name of Installation. 17 | await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); 18 | }); 19 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const eslint = require("@eslint/js"); 3 | const tseslint = require("typescript-eslint"); 4 | const angular = require("angular-eslint"); 5 | 6 | module.exports = tseslint.config( 7 | { 8 | files: ["**/*.ts"], 9 | extends: [ 10 | eslint.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | ...tseslint.configs.stylistic, 13 | ...angular.configs.tsRecommended, 14 | ], 15 | processor: angular.processInlineTemplates, 16 | rules: { 17 | "@angular-eslint/component-selector": [ 18 | "error", 19 | { 20 | type: "element", 21 | prefix: "app", 22 | style: "kebab-case", 23 | }, 24 | ], 25 | }, 26 | }, 27 | { 28 | files: ["**/*.html"], 29 | extends: [ 30 | ...angular.configs.templateRecommended, 31 | ...angular.configs.templateAccessibility, 32 | ], 33 | rules: { 34 | "@angular-eslint/template/label-has-associated-control": "warn", 35 | }, 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realworld-angular-starter", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "prepare": "husky install", 11 | "format": "prettier --write \"src/**/*.{ts,html,css,json,md,yaml,yml}\"", 12 | "format:check": "prettier --check \"src/**/*.{ts,html,css,json,md,yaml,yml}\"", 13 | "lint": "ng lint" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^19.1.1", 18 | "@angular/common": "^19.1.1", 19 | "@angular/compiler": "^19.1.1", 20 | "@angular/core": "^19.1.1", 21 | "@angular/forms": "^19.1.1", 22 | "@angular/platform-browser": "^19.1.1", 23 | "@angular/platform-browser-dynamic": "^19.1.1", 24 | "@angular/router": "^19.1.1", 25 | "rxjs": "~7.8.0", 26 | "tslib": "^2.3.0", 27 | "zone.js": "~0.15.0" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "^19.1.1", 31 | "@angular/cli": "^19.1.1", 32 | "@angular/compiler-cli": "^19.1.1", 33 | "@commitlint/cli": "^19.3.0", 34 | "@commitlint/config-conventional": "^19.2.2", 35 | "@playwright/test": "^1.46.0", 36 | "@types/jasmine": "~5.1.0", 37 | "@types/node": "^22.1.0", 38 | "angular-eslint": "19.0.2", 39 | "axe-playwright": "^2.0.1", 40 | "eslint": "^9.18.0", 41 | "husky": "^9.0.11", 42 | "jasmine-core": "~5.1.0", 43 | "karma": "~6.4.0", 44 | "karma-chrome-launcher": "~3.2.0", 45 | "karma-coverage": "~2.2.0", 46 | "karma-jasmine": "~5.1.0", 47 | "karma-jasmine-html-reporter": "~2.1.0", 48 | "lint-staged": "^15.1.0", 49 | "prettier": "3.3.3", 50 | "stylelint": "^16.8.1", 51 | "stylelint-config-standard": "^36.0.1", 52 | "typescript": "~5.7.0", 53 | "typescript-eslint": "8.20.0" 54 | }, 55 | "lint-staged": { 56 | "src/**/*.{ts,html,css,json,md,yaml,yml}": [ 57 | "prettier --write" 58 | ], 59 | "src/**/*.{ts,js}": [ 60 | "eslint --fix" 61 | ], 62 | "src/**/*.css": [ 63 | "stylelint --fix" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: './e2e', 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env['CI'], 19 | /* Retry on CI only */ 20 | retries: process.env['CI'] ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env['CI'] ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: 'on-first-retry', 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | 41 | { 42 | name: 'firefox', 43 | use: { ...devices['Desktop Firefox'] }, 44 | }, 45 | 46 | { 47 | name: 'webkit', 48 | use: { ...devices['Desktop Safari'] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | // webServer: { 74 | // command: 'npm run start', 75 | // url: 'http://127.0.0.1:3000', 76 | // reuseExistingServer: !process.env.CI, 77 | // }, 78 | }); 79 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/public/favicon.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/add-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/error-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/icons/valid-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/light-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/public/light-logo.gif -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | import { HeaderComponent } from './core/layout/header/header.component'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | imports: [RouterOutlet, HeaderComponent], 8 | templateUrl: './app.component.html', 9 | styleUrl: './app.component.css', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class AppComponent {} 13 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection, inject, provideAppInitializer } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideHttpClient } from '@angular/common/http'; 6 | import { Meta } from '@angular/platform-browser'; 7 | import { environment } from '../environments/environment'; 8 | 9 | export const appConfig: ApplicationConfig = { 10 | providers: [ 11 | provideZoneChangeDetection({ eventCoalescing: true }), 12 | provideRouter(routes), 13 | provideHttpClient(), 14 | provideAppInitializer(() => { 15 | inject(Meta).addTag({ 16 | httpEquiv: 'Content-Security-Policy', 17 | content: environment.csp, 18 | }); 19 | }), 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: 'communities', 6 | loadChildren: () => import('./features/communities/communities.routes'), 7 | }, 8 | { 9 | path: 'events', 10 | loadChildren: () => import('./features/events/events.routes'), 11 | }, 12 | { 13 | path: 'about', 14 | loadChildren: () => import('./features/about/about.routes'), 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/app/core/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | import { provideHttpClientTesting } from '@angular/common/http/testing'; 5 | import { provideHttpClient } from '@angular/common/http'; 6 | 7 | describe('AuthService', () => { 8 | let service: AuthService; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | providers: [provideHttpClientTesting(), provideHttpClient()], 13 | }); 14 | service = TestBed.inject(AuthService); 15 | }); 16 | 17 | it('should be created', () => { 18 | expect(service).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/app/core/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { SignupPayload } from './models/signup-payload.model'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthService { 10 | readonly #http = inject(HttpClient); 11 | 12 | public signup(payload: SignupPayload): Observable { 13 | return this.#http.post('/auth/signup', payload); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.component.css: -------------------------------------------------------------------------------- 1 | form { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | } 6 | 7 | .btn-submit { 8 | font-weight: 700; 9 | margin-block-start: 1rem; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | @if (form.controls.email.hasError("email")) { 5 | Invalid email format 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [LoginComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(LoginComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, output } from '@angular/core'; 2 | import { AsyncPipe } from '@angular/common'; 3 | import { ErrorMessageDirective } from '../../../shared/directives/error-message.directive'; 4 | import { FieldDirective } from '../../../shared/directives/field.directive'; 5 | import { FormDirective } from '../../../shared/directives/form.directive'; 6 | import { FormFieldComponent } from '../../../shared/components/form-field/form-field.component'; 7 | import { 8 | FormControl, 9 | FormGroup, 10 | FormsModule, 11 | ReactiveFormsModule, 12 | Validators, 13 | } from '@angular/forms'; 14 | import { HintMessageDirective } from '../../../shared/directives/hint-message.directive'; 15 | import { LoginPayload } from '../models/login-payload.model'; 16 | 17 | interface FormType { 18 | email: FormControl; 19 | password: FormControl; 20 | } 21 | 22 | @Component({ 23 | selector: 'app-login', 24 | imports: [ 25 | AsyncPipe, 26 | ErrorMessageDirective, 27 | FieldDirective, 28 | FormDirective, 29 | FormFieldComponent, 30 | FormsModule, 31 | HintMessageDirective, 32 | ReactiveFormsModule, 33 | ], 34 | templateUrl: './login.component.html', 35 | styleUrl: './login.component.css', 36 | changeDetection: ChangeDetectionStrategy.OnPush 37 | }) 38 | export class LoginComponent { 39 | readonly submitForm = output(); 40 | 41 | form = new FormGroup({ 42 | email: new FormControl('', { 43 | validators: [Validators.required, Validators.email], // TODO add custom pattern instead of email 44 | nonNullable: true, 45 | }), 46 | password: new FormControl('', { 47 | validators: [Validators.required], 48 | nonNullable: true, 49 | }), 50 | }); 51 | 52 | submit(): void { 53 | this.form.markAllAsTouched(); 54 | 55 | if (this.form.valid) { 56 | this.submitForm.emit(this.form.value as LoginPayload); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/core/auth/models/login-payload.model.ts: -------------------------------------------------------------------------------- 1 | export interface LoginPayload { 2 | email: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/core/auth/models/signup-payload.model.ts: -------------------------------------------------------------------------------- 1 | export interface SignupPayload { 2 | username: string; 3 | email: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/auth/signup/signup.component.css: -------------------------------------------------------------------------------- 1 | form { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | } 6 | 7 | .btn-submit { 8 | font-weight: 700; 9 | margin-block-start: 1rem; 10 | } 11 | 12 | .password-strength-container { 13 | display: flex; 14 | gap: 0.5rem; 15 | justify-content: space-between; 16 | align-items: center; 17 | } 18 | 19 | .level-list { 20 | display: flex; 21 | justify-content: space-between; 22 | gap: 0.3rem; 23 | width: 100%; 24 | } 25 | 26 | .level { 27 | height: 0.3rem; 28 | width: 100%; 29 | } 30 | 31 | .level-active { 32 | background-color: var(--success-color); 33 | } 34 | 35 | .password-strength-label { 36 | font-size: 0.7rem; 37 | min-width: 4rem; 38 | text-align: end; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/core/auth/signup/signup.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | @if (form.controls.email.hasError("email")) { 8 | Invalid email format 9 | } 10 | 11 | 12 | 13 | 14 | @let passwordStrength = passwordStrength$ | async; 15 |
16 | @for (level of passwordLevels; track level; let index = $index) { 17 |
21 | } 22 |
23 |
24 | {{ passwordLevels[passwordStrength] }} 25 |
26 |
27 |
28 | 29 | 30 | @if (form.controls.confirmPassword.hasError("passwordMismatch")) { 31 | Passwords do not match 32 | } 33 | 34 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /src/app/core/auth/signup/signup.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SignupComponent } from './signup.component'; 4 | 5 | describe('SignupComponent', () => { 6 | let component: SignupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SignupComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(SignupComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/core/auth/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, output } from '@angular/core'; 2 | import { 3 | AbstractControl, 4 | FormControl, 5 | FormGroup, 6 | ReactiveFormsModule, 7 | ValidationErrors, 8 | ValidatorFn, 9 | Validators, 10 | } from '@angular/forms'; 11 | import { FormFieldComponent } from '../../../shared/components/form-field/form-field.component'; 12 | import { FieldDirective } from '../../../shared/directives/field.directive'; 13 | import { ErrorMessageDirective } from '../../../shared/directives/error-message.directive'; 14 | import { FormDirective } from '../../../shared/directives/form.directive'; 15 | import { map } from 'rxjs'; 16 | import { AsyncPipe } from '@angular/common'; 17 | import { SignupPayload } from '../models/signup-payload.model'; 18 | import { HintMessageDirective } from '../../../shared/directives/hint-message.directive'; 19 | 20 | interface FormType { 21 | username: FormControl; 22 | email: FormControl; 23 | password: FormControl; 24 | confirmPassword: FormControl; 25 | } 26 | 27 | @Component({ 28 | selector: 'app-signup', 29 | imports: [ 30 | ReactiveFormsModule, 31 | FormFieldComponent, 32 | FieldDirective, 33 | ErrorMessageDirective, 34 | FormDirective, 35 | AsyncPipe, 36 | HintMessageDirective, 37 | ], 38 | templateUrl: './signup.component.html', 39 | styleUrl: './signup.component.css', 40 | changeDetection: ChangeDetectionStrategy.OnPush 41 | }) 42 | export class SignupComponent { 43 | readonly submitForm = output(); 44 | 45 | form = new FormGroup({ 46 | username: new FormControl('', { 47 | validators: [Validators.required], 48 | nonNullable: true, 49 | }), 50 | email: new FormControl('', { 51 | validators: [Validators.required, Validators.email], // TODO add custom pattern instead of email 52 | nonNullable: true, 53 | }), 54 | password: new FormControl('', { 55 | validators: [Validators.required], 56 | nonNullable: true, 57 | }), 58 | confirmPassword: new FormControl('', { 59 | validators: [Validators.required, this.validatePasswordMatch()], 60 | nonNullable: true, 61 | }), 62 | }); 63 | 64 | passwordLevels = ['weak', 'fair', 'good', 'strong', 'very strong']; 65 | passwordStrength$ = this.form.controls.password.valueChanges.pipe( 66 | map((password) => { 67 | let strength = 0; 68 | 69 | if (password.length >= 8) strength++; 70 | if (/[A-Z]/.test(password)) strength++; 71 | if (/[a-z]/.test(password)) strength++; 72 | if (/\d/.test(password)) strength++; 73 | if (/[\W_]/.test(password)) strength++; 74 | 75 | return strength; 76 | }), 77 | ); 78 | 79 | submit(): void { 80 | this.form.markAllAsTouched(); 81 | 82 | if (this.form.valid) { 83 | this.submitForm.emit(this.form.value as SignupPayload); 84 | } 85 | } 86 | 87 | validatePasswordMatch(): ValidatorFn { 88 | return (control: AbstractControl): ValidationErrors | null => { 89 | if (!control.parent) { 90 | return null; 91 | } 92 | 93 | const form = control.parent as FormGroup; 94 | 95 | const password = form.value.password; 96 | const confirmPassword = form.value.confirmPassword; 97 | 98 | return password === confirmPassword ? null : { passwordMismatch: true }; 99 | }; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/core/layout/header/header.component.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | gap: 5rem; 6 | padding-inline: 1rem; 7 | } 8 | 9 | .logo:focus { 10 | border-radius: var(--border-radius); 11 | outline: 2px solid var(--primary-color); 12 | } 13 | 14 | .static-logo { 15 | display: none; 16 | } 17 | 18 | .nav-container { 19 | flex: 1; 20 | } 21 | 22 | .nav-list { 23 | display: flex; 24 | gap: 2rem; 25 | } 26 | 27 | .auth-buttons { 28 | display: flex; 29 | gap: 1rem; 30 | } 31 | 32 | .active-link { 33 | border-bottom-color: var(--primary-color); 34 | color: var(--primary-color); 35 | } 36 | 37 | @media (prefers-reduced-motion: reduce) { 38 | .animated-logo { 39 | display: none; 40 | } 41 | 42 | .static-logo { 43 | display: block; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/core/layout/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 18 | 40 |
41 | 42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/app/core/layout/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HeaderComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(HeaderComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/core/layout/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { RouterLink, RouterLinkActive } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | templateUrl: './header.component.html', 7 | styleUrl: './header.component.css', 8 | imports: [RouterLink, RouterLinkActive], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | }) 11 | export class HeaderComponent {} 12 | -------------------------------------------------------------------------------- /src/app/features/about/about.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | const ABOUT_ROUTES: Routes = [ 4 | { 5 | path: '', 6 | loadComponent: () => import('./pages/about/about.component'), 7 | }, 8 | ]; 9 | 10 | export default ABOUT_ROUTES; 11 | -------------------------------------------------------------------------------- /src/app/features/about/pages/about/about.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/app/features/about/pages/about/about.component.css -------------------------------------------------------------------------------- /src/app/features/about/pages/about/about.component.html: -------------------------------------------------------------------------------- 1 |

about works!

2 | -------------------------------------------------------------------------------- /src/app/features/about/pages/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import AboutComponent from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [AboutComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(AboutComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/features/about/pages/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about', 5 | imports: [], 6 | templateUrl: './about.component.html', 7 | styleUrl: './about.component.css', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export default class AboutComponent {} 11 | -------------------------------------------------------------------------------- /src/app/features/communities/communities.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | const COMMUNITIES_ROUTES: Routes = [ 4 | { 5 | path: '', 6 | loadComponent: () => 7 | import('./pages/community-list/community-list.component'), 8 | }, 9 | ]; 10 | 11 | export default COMMUNITIES_ROUTES; 12 | -------------------------------------------------------------------------------- /src/app/features/communities/pages/community-list/community-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/app/features/communities/pages/community-list/community-list.component.css -------------------------------------------------------------------------------- /src/app/features/communities/pages/community-list/community-list.component.html: -------------------------------------------------------------------------------- 1 |

community-list works!

2 | -------------------------------------------------------------------------------- /src/app/features/communities/pages/community-list/community-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import CommunityListComponent from './community-list.component'; 4 | 5 | describe('CommunityListComponent', () => { 6 | let component: CommunityListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CommunityListComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(CommunityListComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/features/communities/pages/community-list/community-list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-community-list', 5 | imports: [], 6 | templateUrl: './community-list.component.html', 7 | styleUrl: './community-list.component.css', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export default class CommunityListComponent {} 11 | -------------------------------------------------------------------------------- /src/app/features/events/events.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | const EVENT_ROUTES: Routes = [ 4 | { 5 | path: '', 6 | loadComponent: () => import('./pages/event-list/event-list.component'), 7 | }, 8 | ]; 9 | 10 | export default EVENT_ROUTES; 11 | -------------------------------------------------------------------------------- /src/app/features/events/pages/event-list/event-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/app/features/events/pages/event-list/event-list.component.css -------------------------------------------------------------------------------- /src/app/features/events/pages/event-list/event-list.component.html: -------------------------------------------------------------------------------- 1 |

event-list works!

2 | -------------------------------------------------------------------------------- /src/app/features/events/pages/event-list/event-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import EventListComponent from './event-list.component'; 4 | 5 | describe('EventListComponent', () => { 6 | let component: EventListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [EventListComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(EventListComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/features/events/pages/event-list/event-list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-event-list', 5 | imports: [], 6 | templateUrl: './event-list.component.html', 7 | styleUrl: './event-list.component.css', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export default class EventListComponent {} 11 | -------------------------------------------------------------------------------- /src/app/shared/components/form-field/form-field.component.css: -------------------------------------------------------------------------------- 1 | .field { 2 | all: unset; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .label-container { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 0.3rem; 11 | } 12 | 13 | .label-text { 14 | font-size: 0.875rem; 15 | font-weight: 500; 16 | } 17 | 18 | .error-container { 19 | display: flex; 20 | flex-direction: column; 21 | min-height: 1.25rem; 22 | padding: 0.4rem; 23 | line-height: 1rem; 24 | } 25 | 26 | .focused { 27 | outline-offset: 2px; 28 | outline: var(--neutral-color-300) solid 2px; 29 | } 30 | 31 | .field-input { 32 | height: 2rem; 33 | border-width: 1px; 34 | border-style: solid; 35 | border-color: var(--neutral-color-300); 36 | border-radius: var(--border-radius); 37 | background-color: var(--neutral-color-700); 38 | padding-inline: 1rem 0.5rem; 39 | display: flex; 40 | gap: 1rem; 41 | } 42 | 43 | .field-validation { 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | min-width: 1.5rem; 48 | } 49 | 50 | .invalid { 51 | border-color: var(--error-color); 52 | } 53 | 54 | .required { 55 | color: var(--error-color); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/shared/components/form-field/form-field.component.html: -------------------------------------------------------------------------------- 1 |
2 | 35 |
36 | @if (control().touched) { 37 | @if (control().invalid) { 38 | @if (!control().hasError("required")) { 39 | 40 | } 41 | @if (control().hasError("required")) { 42 | Required field 43 | } 44 | } 45 | } 46 | 47 | @if (!control().hasError("required") && !extraErrorMessages().length) { 48 | 49 | } 50 |
51 |
52 | -------------------------------------------------------------------------------- /src/app/shared/components/form-field/form-field.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FormFieldComponent } from './form-field.component'; 4 | import { FormDirective } from '../../directives/form.directive'; 5 | 6 | describe('FormFieldComponent', () => { 7 | let component: FormFieldComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [FormFieldComponent], 13 | providers: [FormDirective], 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(FormFieldComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/shared/components/form-field/form-field.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewChecked, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | computed, 7 | contentChild, 8 | contentChildren, 9 | inject, 10 | input, 11 | } from '@angular/core'; 12 | import { NgControl, Validators } from '@angular/forms'; 13 | import { 14 | AsyncPipe, 15 | LowerCasePipe, 16 | NgIf, 17 | NgOptimizedImage, 18 | } from '@angular/common'; 19 | import { ErrorMessageDirective } from '../../directives/error-message.directive'; 20 | import { FieldDirective } from '../../directives/field.directive'; 21 | import { FormDirective } from '../../directives/form.directive'; 22 | 23 | @Component({ 24 | selector: 'app-form-field', 25 | imports: [ 26 | AsyncPipe, 27 | NgIf, 28 | ErrorMessageDirective, 29 | LowerCasePipe, 30 | NgOptimizedImage, 31 | ], 32 | templateUrl: './form-field.component.html', 33 | styleUrl: './form-field.component.css', 34 | changeDetection: ChangeDetectionStrategy.OnPush 35 | }) 36 | export class FormFieldComponent implements AfterViewChecked { 37 | cdr = inject(ChangeDetectorRef); 38 | 39 | label = input.required(); 40 | control = contentChild.required(NgControl); 41 | field = contentChild.required(FieldDirective); 42 | isOptionalEnforced = inject(FormDirective).isOptionalEnforced; 43 | extraErrorMessages = contentChildren(ErrorMessageDirective); 44 | 45 | ngAfterViewChecked() { 46 | this.cdr.detectChanges(); 47 | } 48 | 49 | isRequired = computed(() => 50 | this.control().control?.hasValidator(Validators.required), 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/shared/components/vertical-nav/vertical-nav.component.css: -------------------------------------------------------------------------------- 1 | .vertical-container { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 0.5rem; 5 | } 6 | 7 | .vertical-link { 8 | display: flex; 9 | align-items: center; 10 | gap: 0.5rem; 11 | color: var(--neutral-color-600); 12 | border-radius: var(--border-radius); 13 | padding: 0.25rem 0.5rem; 14 | } 15 | 16 | .vertical-link-icon { 17 | filter: brightness(0) saturate(100%) invert(40%) sepia(0%) saturate(0%) 18 | hue-rotate(68deg) brightness(95%) contrast(91%); 19 | } 20 | 21 | .active, 22 | .vertical-link:hover { 23 | color: var(--neutral-color-100); 24 | background-color: var(--neutral-color-500); 25 | } 26 | 27 | .vertical-link:focus { 28 | color: var(--neutral-color-100); 29 | outline: 2px solid var(--neutral-color-light); 30 | } 31 | 32 | .vertical-link:hover .vertical-link-icon, 33 | .vertical-link:focus .vertical-link-icon { 34 | filter: brightness(0) saturate(100%) invert(100%) sepia(2%) saturate(438%) 35 | hue-rotate(202deg) brightness(113%) contrast(78%); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/shared/components/vertical-nav/vertical-nav.component.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/app/shared/components/vertical-nav/vertical-nav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VerticalNavComponent } from './vertical-nav.component'; 4 | 5 | describe('VerticalNavComponent', () => { 6 | let component: VerticalNavComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [VerticalNavComponent], 12 | }).compileComponents(); 13 | 14 | fixture = TestBed.createComponent(VerticalNavComponent); 15 | component = fixture.componentInstance; 16 | }); 17 | 18 | it('should create', () => { 19 | fixture.componentRef.setInput('links', []); 20 | fixture.detectChanges(); 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/shared/components/vertical-nav/vertical-nav.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, input } from '@angular/core'; 2 | import { VerticalLink } from '../../models/vertical-link.model'; 3 | import { NgOptimizedImage } from '@angular/common'; 4 | import { RouterLink, RouterLinkActive } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-vertical-nav', 8 | imports: [NgOptimizedImage, RouterLink, RouterLinkActive], 9 | templateUrl: './vertical-nav.component.html', 10 | styleUrl: './vertical-nav.component.css', 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class VerticalNavComponent { 14 | links = input.required(); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/directives/error-message.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessageDirective } from './error-message.directive'; 2 | 3 | describe('ErrorMessageDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new ErrorMessageDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/error-message.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | /* 4 | Styles form error messages and create reference for FieldComponent ContentChildren 5 | */ 6 | @Directive({ 7 | selector: 'app-error-message', 8 | host: { 9 | class: 'field-error-message', 10 | }, 11 | }) 12 | export class ErrorMessageDirective {} 13 | -------------------------------------------------------------------------------- /src/app/shared/directives/field.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { FieldDirective } from './field.directive'; 2 | 3 | describe('FieldDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new FieldDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | 9 | // TODO add proper testing with Testing Library 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/shared/directives/field.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener, signal } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[app-field]', 5 | }) 6 | export class FieldDirective { 7 | isFocus = signal(false); 8 | 9 | @HostListener('focus') 10 | onFocus() { 11 | this.isFocus.set(true); 12 | } 13 | 14 | @HostListener('blur') 15 | onBlur() { 16 | this.isFocus.set(false); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/directives/form.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormDirective } from './form.directive'; 2 | 3 | describe('FormDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new FormDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/form.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, input } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'form[formGroup]', 5 | }) 6 | export class FormDirective { 7 | // Error: NG0203: inputFunction() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://angular.dev/errors/NG0203 8 | isOptionalEnforced = input(); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/directives/hint-message.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { HintMessageDirective } from './hint-message.directive'; 2 | 3 | describe('HintMessageDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new HintMessageDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/directives/hint-message.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'app-hint-message', 5 | }) 6 | export class HintMessageDirective {} 7 | -------------------------------------------------------------------------------- /src/app/shared/models/vertical-link.model.ts: -------------------------------------------------------------------------------- 1 | export interface VerticalLink { 2 | label: string; 3 | url: string; 4 | icon: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: '', 3 | csp: ` 4 | default-src 'self'; 5 | script-src 'self' 'nonce-randomNonceGoesHere'; 6 | style-src 'self' 'nonce-randomNonceGoesHere'; 7 | font-src 'self'; 8 | img-src 'self' data:; 9 | object-src 'none'; 10 | base-uri 'self'; 11 | form-action 'self'; 12 | connect-src 'self'; 13 | `, 14 | }; 15 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | const API_URL = 'https://playful-rugelach-3258d8.netlify.app'; 2 | 3 | export const environment = { 4 | apiUrl: API_URL, 5 | csp: ` 6 | default-src 'self'; 7 | script-src 'self' 'nonce-randomNonceGoesHere'; 8 | style-src 'self' 'nonce-randomNonceGoesHere'; 9 | font-src 'self'; 10 | img-src 'self' data:; 11 | object-src 'none'; 12 | base-uri 'self'; 13 | form-action 'self'; 14 | connect-src 'self' ${API_URL}; 15 | `, 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Realworld Angular starter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => 6 | console.error(err), 7 | ); 8 | -------------------------------------------------------------------------------- /src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3000", 4 | "secure": false, 5 | "changeOrigin": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url("./styles/reset.css"); 3 | @import url("./styles/components/buttons.css"); 4 | @import url("./styles/components/form.css"); 5 | @import url("./styles/tokens/colors.css"); 6 | @import url("./styles/tokens/shapes.css"); 7 | 8 | body { 9 | font-family: Inter, sans-serif; 10 | color: var(--text-color); 11 | background-color: var(--background-color); 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/components/buttons.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | color: var(--text-color); 3 | background-color: transparent; 4 | padding: 0.5rem 1rem; 5 | border-bottom: 2px solid transparent; 6 | font-weight: 500; 7 | line-height: 1.25rem; 8 | cursor: pointer; 9 | text-align: center; 10 | } 11 | 12 | .nav-link:hover { 13 | color: var(--primary-color); 14 | border-bottom-color: var(--primary-color); 15 | } 16 | 17 | .nav-link:focus { 18 | color: var(--primary-color); 19 | border-bottom-color: var(--primary-color); 20 | } 21 | 22 | .btn { 23 | padding: 0.5rem 1rem; 24 | font-weight: 500; 25 | line-height: 1.25rem; 26 | } 27 | 28 | .btn, 29 | .btn-icon { 30 | display: grid; 31 | place-items: center; 32 | border-radius: var(--border-radius); 33 | cursor: pointer; 34 | color: var(--text-color); 35 | } 36 | 37 | .btn-icon { 38 | height: 3rem; 39 | width: 3rem; 40 | font-size: 2rem; 41 | line-height: 2rem; 42 | } 43 | 44 | .btn-icon img { 45 | filter: var(--svg-color-filter); 46 | } 47 | 48 | .btn:hover, 49 | .btn-icon:hover { 50 | background-color: var(--neutral-color-200); 51 | } 52 | 53 | .btn:focus, 54 | .btn-icon:focus { 55 | outline-offset: 2px; 56 | outline: var(--neutral-color) solid 2px; 57 | } 58 | 59 | .btn-outlined { 60 | border: 2px solid var(--neutral-color); 61 | } 62 | 63 | .btn-primary { 64 | background-color: var(--primary-bg-color); 65 | color: var(--text-color-invert); 66 | } 67 | 68 | .btn-primary:focus { 69 | outline-offset: 2px; 70 | outline: var(--primary-color) solid 2px; 71 | } 72 | 73 | .btn-primary:hover { 74 | background-color: var(--primary-bg-color-hover); 75 | } 76 | 77 | .btn-secondary { 78 | background-color: var(--secondary-bg-color); 79 | color: var(--text-color-invert); 80 | } 81 | 82 | .btn-secondary:focus { 83 | outline-offset: 2px; 84 | outline: var(--secondary-color) solid 2px; 85 | } 86 | 87 | .btn-secondary:hover { 88 | background-color: var(--secondary-bg-color-hover); 89 | } 90 | -------------------------------------------------------------------------------- /src/styles/components/card.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/styles/components/card.css -------------------------------------------------------------------------------- /src/styles/components/event.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/styles/components/event.css -------------------------------------------------------------------------------- /src/styles/components/form.css: -------------------------------------------------------------------------------- 1 | input { 2 | all: unset; 3 | height: 100%; 4 | width: 100%; 5 | } 6 | 7 | .field-error-message { 8 | color: var(--error-color); 9 | font-size: 0.75rem; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/components/layout.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/styles/components/layout.css -------------------------------------------------------------------------------- /src/styles/components/post.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realworld-angular/realworld-angular-starter/521292449a14531cc283c3f049b072ae70227bc9/src/styles/components/post.css -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style-type: none; 3 | padding: 0; 4 | } 5 | 6 | button { 7 | border: none; 8 | } 9 | 10 | a { 11 | text-decoration: none; 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/tokens/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-hue: 180; 3 | --primary-saturation: 50%; 4 | --primary-color-100: hsl(var(--primary-hue) var(--primary-saturation) 94%); 5 | --primary-color-200: hsl(var(--primary-hue) var(--primary-saturation) 84%); 6 | --primary-color-300: hsl(var(--primary-hue) var(--primary-saturation) 74%); 7 | --primary-color-400: hsl(var(--primary-hue) var(--primary-saturation) 64%); 8 | --primary-color-500: hsl( 9 | var(--primary-hue) var(--primary-saturation) 54% 10 | ); /* Base color */ 11 | 12 | --primary-color-600: hsl(var(--primary-hue) var(--primary-saturation) 44%); 13 | --primary-color-700: hsl(var(--primary-hue) var(--primary-saturation) 34%); 14 | --primary-color-800: hsl(var(--primary-hue) var(--primary-saturation) 24%); 15 | --primary-color-900: hsl(var(--primary-hue) var(--primary-saturation) 14%); 16 | --primary-color: var(--primary-color-600); 17 | --primary-bg-color: var(--primary-color-500); 18 | --primary-bg-color-hover: var(--primary-color-600); 19 | --secondary-hue: 168; 20 | --secondary-saturation: 80%; 21 | --secondary-color-100: hsl( 22 | var(--secondary-hue) var(--secondary-saturation) 94% 23 | ); 24 | --secondary-color-200: hsl( 25 | var(--secondary-hue) var(--secondary-saturation) 84% 26 | ); 27 | --secondary-color-300: hsl( 28 | var(--secondary-hue) var(--secondary-saturation) 74% 29 | ); 30 | --secondary-color-400: hsl( 31 | var(--secondary-hue) var(--secondary-saturation) 64% 32 | ); 33 | --secondary-color-500: hsl( 34 | var(--secondary-hue) var(--secondary-saturation) 54% 35 | ); /* Base color */ 36 | 37 | --secondary-color-600: hsl( 38 | var(--secondary-hue) var(--secondary-saturation) 44% 39 | ); 40 | --secondary-color-700: hsl( 41 | var(--secondary-hue) var(--secondary-saturation) 34% 42 | ); 43 | --secondary-color-800: hsl( 44 | var(--secondary-hue) var(--secondary-saturation) 24% 45 | ); 46 | --secondary-color-900: hsl( 47 | var(--secondary-hue) var(--secondary-saturation) 14% 48 | ); 49 | --secondary-color: var(--secondary-color-500); 50 | --secondary-bg-color: var(--secondary-color-500); 51 | --secondary-bg-color-hover: var(--secondary-color-600); 52 | --neutral-hue: 0; 53 | --neutral-saturation: 0%; 54 | --neutral-color-100: hsl(var(--neutral-hue) var(--neutral-saturation) 94%); 55 | --neutral-color-200: hsl(var(--neutral-hue) var(--neutral-saturation) 84%); 56 | --neutral-color-300: hsl(var(--neutral-hue) var(--neutral-saturation) 74%); 57 | --neutral-color-400: hsl(var(--neutral-hue) var(--neutral-saturation) 64%); 58 | --neutral-color-500: hsl(var(--neutral-hue) var(--neutral-saturation) 54%); 59 | --neutral-color-600: hsl(var(--neutral-hue) var(--neutral-saturation) 44%); 60 | --neutral-color-700: hsl(var(--neutral-hue) var(--neutral-saturation) 34%); 61 | --neutral-color-800: hsl(var(--neutral-hue) var(--neutral-saturation) 24%); 62 | --neutral-color-900: hsl(var(--neutral-hue) var(--neutral-saturation) 14%); 63 | --neutral-color: var(--neutral-color-500); 64 | --neutral-bg-color: var(--neutral-color-500); 65 | --neutral-bg-color-hover: var(--neutral-color-600); 66 | --text-color: var(--neutral-color-900); 67 | --text-color-invert: var(--neutral-color-100); 68 | --background-color: var(--neutral-color-100); 69 | --error-color: #cf6679; 70 | --success-color: #0f0; 71 | --svg-color-filter: brightness(0) saturate(100%) invert(0%) sepia(3%) 72 | saturate(2922%) hue-rotate(316deg) brightness(98%) contrast(81%); 73 | } 74 | -------------------------------------------------------------------------------- /src/styles/tokens/shapes.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --border-radius: 0.25rem; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "sourceMap": false, 15 | "inlineSourceMap": false, 16 | "isolatedModules": true, 17 | "declaration": false, 18 | "experimentalDecorators": true, 19 | "moduleResolution": "bundler", 20 | "importHelpers": true, 21 | "target": "ES2022", 22 | "module": "ES2022", 23 | "lib": ["ES2022", "dom"] 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------