diff --git a/.all-contributorsrc b/.all-contributorsrc
index a6566ffe9..6f2f57401 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -12,7 +12,27 @@
"types": {
"challenge": {
"symbol": "🧩",
- "description": "Created a challenge"
+ "description": "Create a challenge"
+ },
+ "translation-es": {
+ "symbol": "🇪🇸",
+ "description": "Translate in Spanish"
+ },
+ "translation-fr": {
+ "symbol": "🇫🇷",
+ "description": "Translate in French"
+ },
+ "translation-pt": {
+ "symbol": "🇵🇹",
+ "description": "Translate in Portuguese"
+ },
+ "translation-ru": {
+ "symbol": "🇷🇺",
+ "description": "Translate in Russian"
+ },
+ "translation-ch": {
+ "symbol": "🇨🇳",
+ "description": "Translate in Chinese"
}
},
"contributors": [
@@ -27,9 +47,91 @@
"doc",
"content",
"ideas",
+ "design",
+ "translation-fr"
+ ]
+ },
+ {
+ "login": "svenson95",
+ "name": "Sven Brodny",
+ "avatar_url": "https://avatars.githubusercontent.com/u/46655156?v=4",
+ "profile": "https://svenson95.github.io/sb-portfolio/",
+ "contributions": [
+ "doc",
+ "challenge",
+ "content",
"design"
]
},
+ {
+ "login": "jdegand",
+ "name": "J. Degand",
+ "avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
+ "profile": "https://github.com/jdegand",
+ "contributions": [
+ "doc",
+ "content",
+ "code"
+ ]
+ },
+ {
+ "login": "DeveshChau",
+ "name": "Devesh Chaudhari",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9509673?v=4",
+ "profile": "https://github.com/DeveshChau",
+ "contributions": [
+ "code",
+ "bug",
+ "challenge"
+ ]
+ },
+ {
+ "login": "stillst",
+ "name": "stillst",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1463098?v=4",
+ "profile": "https://github.com/stillst",
+ "contributions": [
+ "challenge",
+ "translation-ru"
+ ]
+ },
+ {
+ "login": "wandri",
+ "name": "Wandrille",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15016833?v=4",
+ "profile": "https://wandrille-guesdon.com/",
+ "contributions": [
+ "challenge"
+ ]
+ },
+ {
+ "login": "alcaidio",
+ "name": "Timothy Alcaide",
+ "avatar_url": "https://avatars.githubusercontent.com/u/17033036?v=4",
+ "profile": "https://twitter.com/alcaidio",
+ "contributions": [
+ "challenge"
+ ]
+ },
+ {
+ "login": "LMFinney",
+ "name": "Lance Finney",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6683747?v=4",
+ "profile": "https://github.com/LMFinney",
+ "contributions": [
+ "doc",
+ "challenge"
+ ]
+ },
+ {
+ "login": "tsironis13",
+ "name": "Tsironis Ioannis",
+ "avatar_url": "https://avatars.githubusercontent.com/u/7561447?v=4",
+ "profile": "https://github.com/tsironis13",
+ "contributions": [
+ "challenge"
+ ]
+ },
{
"login": "alan-bio",
"name": "Alan Dragicevich",
@@ -78,39 +180,186 @@
]
},
{
- "login": "jdegand",
- "name": "J. Degand",
- "avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
- "profile": "https://github.com/jdegand",
+ "login": "dmmishchenko",
+ "name": "Dmitriy Mishchenko",
+ "avatar_url": "https://avatars.githubusercontent.com/u/51910160?v=4",
+ "profile": "https://github.com/dmmishchenko",
"contributions": [
"doc"
]
},
{
- "login": "DeveshChau",
- "name": "Devesh Chaudhari",
- "avatar_url": "https://avatars.githubusercontent.com/u/9509673?v=4",
- "profile": "https://github.com/DeveshChau",
+ "login": "Sagardevkota",
+ "name": "Sagar Devkota",
+ "avatar_url": "https://avatars.githubusercontent.com/u/30800393?v=4",
+ "profile": "http://www.sagardev.com.np/",
+ "contributions": [
+ "doc",
+ "code"
+ ]
+ },
+ {
+ "login": "nelsongutidev",
+ "name": "Nelson Gutierrez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/62297014?v=4",
+ "profile": "https://nelsonguti.dev/",
+ "contributions": [
+ "translation-es"
+ ]
+ },
+ {
+ "login": "ho-ssain",
+ "name": "Hossain K. M.",
+ "avatar_url": "https://avatars.githubusercontent.com/u/61125174?v=4",
+ "profile": "https://github.com/ho-ssain",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "kabrunko-dev",
+ "name": "Diogo Nishikawa",
+ "avatar_url": "https://avatars.githubusercontent.com/u/142346548?v=4",
+ "profile": "https://github.com/kabrunko-dev/",
"contributions": [
"code",
- "bug",
- "challenge"
+ "translation-pt",
+ "doc"
]
},
{
- "login": "dmmishchenko",
- "name": "Dmitriy Mishchenko",
- "avatar_url": "https://avatars.githubusercontent.com/u/51910160?v=4",
- "profile": "https://github.com/dmmishchenko",
+ "login": "ErickRodrCodes",
+ "name": "Erick Rodriguez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1978642?v=4",
+ "profile": "http://www.streamoverlaypro.com/",
+ "contributions": [
+ "translation-es"
+ ]
+ },
+ {
+ "login": "eduardoRoth",
+ "name": "Eduardo Roth",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5419161?v=4",
+ "profile": "https://eduardoroth.dev/",
+ "contributions": [
+ "doc",
+ "translation-es"
+ ]
+ },
+ {
+ "login": "1fbr",
+ "name": "Fernando Bello",
+ "avatar_url": "https://avatars.githubusercontent.com/u/63980689?v=4",
+ "profile": "https://github.com/1fbr",
"contributions": [
"doc"
]
},
{
- "login": "Sagardevkota",
- "name": "Sagar Devkota",
- "avatar_url": "https://avatars.githubusercontent.com/u/30800393?v=4",
- "profile": "http://www.sagardev.com.np/",
+ "login": "webbomj",
+ "name": "Лапин Андрей (Lapin Andrey)",
+ "avatar_url": "https://avatars.githubusercontent.com/u/86595717?v=4",
+ "profile": "https://github.com/webbomj",
+ "contributions": [
+ "translation-ru"
+ ]
+ },
+ {
+ "login": "Dinozavvvr",
+ "name": "Dinar Shagaliev",
+ "avatar_url": "https://avatars.githubusercontent.com/u/45518871?v=4",
+ "profile": "https://github.com/Dinozavvvr",
+ "contributions": [
+ "translation-ru"
+ ]
+ },
+ {
+ "login": "vimulatus",
+ "name": "Vimulatus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/63696128?v=4",
+ "profile": "https://github.com/vimulatus",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "alannelucq",
+ "name": "Arthur LANNELUCQ",
+ "avatar_url": "https://avatars.githubusercontent.com/u/44091408?v=4",
+ "profile": "https://github.com/alannelucq",
+ "contributions": [
+ "translation-fr"
+ ]
+ },
+ {
+ "login": "fixedmichal",
+ "name": "fixed_michal",
+ "avatar_url": "https://avatars.githubusercontent.com/u/26270192?v=4",
+ "profile": "https://github.com/fixedmichal",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "Tenessy",
+ "name": "Tenessy",
+ "avatar_url": "https://avatars.githubusercontent.com/u/65855673?v=4",
+ "profile": "https://github.com/Tenessy",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "EnochGao",
+ "name": "Enoch Gao",
+ "avatar_url": "https://avatars.githubusercontent.com/u/41459067?v=4",
+ "profile": "https://enochgao.github.io/",
+ "contributions": [
+ "doc",
+ "translation-ch"
+ ]
+ },
+ {
+ "login": "fpalmab",
+ "name": "Francisco Palma",
+ "avatar_url": "https://avatars.githubusercontent.com/u/7729812?v=4",
+ "profile": "https://github.com/fpalmab",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "michalgrzegorczyk-dev",
+ "name": "Michał Grzegorczyk",
+ "avatar_url": "https://avatars.githubusercontent.com/u/47832176?v=4",
+ "profile": "https://github.com/michalgrzegorczyk-dev",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "tamim36",
+ "name": "Tamim Arefin Anik",
+ "avatar_url": "https://avatars.githubusercontent.com/u/42251521?v=4",
+ "profile": "https://github.com/tamim36",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "WhoisBsa",
+ "name": "Matheus B.",
+ "avatar_url": "https://avatars.githubusercontent.com/u/36895235?v=4",
+ "profile": "https://github.com/WhoisBsa",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "StefH",
+ "name": "Stef Heyenrath",
+ "avatar_url": "https://avatars.githubusercontent.com/u/249938?v=4",
+ "profile": "https://sourcerer.io/stefh",
"contributions": [
"doc"
]
diff --git a/.eslintrc.json b/.eslintrc.json
index c222fb084..de9b234b6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,6 +6,7 @@
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
+ "@angular-eslint/no-host-metadata-property": "off",
"@nx/enforce-module-boundaries": [
"error",
{
@@ -18,24 +19,24 @@
}
]
}
- ],
- "@angular-eslint/no-host-metadata-property": [
- "error",
- {
- "allowStatic": true
- }
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
- "rules": {}
+ "rules": {
+ "@typescript-eslint/no-extra-semi": "error",
+ "no-extra-semi": "off"
+ }
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
- "rules": {}
+ "rules": {
+ "@typescript-eslint/no-extra-semi": "error",
+ "no-extra-semi": "off"
+ }
},
{
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
diff --git a/.github/funding.yml b/.github/funding.yml
new file mode 100644
index 000000000..f1327048f
--- /dev/null
+++ b/.github/funding.yml
@@ -0,0 +1 @@
+github: [tomalaforge]
diff --git a/.github/github-action/action.yml b/.github/github-action/action.yml
new file mode 100644
index 000000000..8019592af
--- /dev/null
+++ b/.github/github-action/action.yml
@@ -0,0 +1,14 @@
+name: 'Hello World'
+description: 'Greet someone and record the time'
+inputs:
+ github_token:
+ description: A GitHub token.
+ required: false
+ default: ${{ github.token }}
+ repo:
+ description: The owner and repository name.
+ required: false
+ default: ${{ github.repository }}
+runs:
+ using: 'node20'
+ main: 'index.js'
diff --git a/.github/github-action/contributors.js b/.github/github-action/contributors.js
new file mode 100644
index 000000000..9555e6633
--- /dev/null
+++ b/.github/github-action/contributors.js
@@ -0,0 +1,34 @@
+const contributors = [
+ 'alcaidio',
+ 'svenson95',
+ 'jdegand',
+ 'DeveshChau',
+ 'stillst',
+ 'wandri',
+ 'webbomj',
+ 'kabrunko-dev',
+ 'Sanjar1304',
+ 'tsironis13',
+ 'EnochGao',
+];
+
+const sponsors = [
+ 'ddotx',
+ 'LMFinney',
+ 'alannelucq',
+ 'SidV2',
+ 'fpalmab',
+ 'CivilEngeneer',
+ 'apalaio',
+ 'amosISA',
+ 'michalgrzegorczyk-dev',
+ 'zealotrahl',
+ 'DzoeL123',
+ 'allan1989',
+ 'pchessah',
+];
+
+module.exports = {
+ contributors,
+ sponsors,
+};
diff --git a/.github/github-action/index.js b/.github/github-action/index.js
new file mode 100644
index 000000000..a73e6c293
--- /dev/null
+++ b/.github/github-action/index.js
@@ -0,0 +1,45 @@
+const github = require('@actions/github');
+const core = require('@actions/core');
+const { contributors, sponsors } = require('./contributors');
+
+async function run() {
+ try {
+ const title = github.context.payload.pull_request.title;
+ const labels = ['answer'];
+
+ const match = title.match(/Answer(:?)\s*(\d+)/);
+ if (match) {
+ labels.push(String(parseInt(match[2], 10)));
+ }
+
+ const actor = github.context.actor;
+ if (contributors.includes(actor)) {
+ labels.push('contributor');
+ labels.push('to be reviewed');
+ }
+
+ if (sponsors.includes(actor)) {
+ labels.push('sponsor');
+ labels.push('to be reviewed');
+ }
+
+ const githubToken = core.getInput('github_token');
+
+ const number = github.context.payload.pull_request.number;
+
+ const octokit = github.getOctokit(githubToken);
+ await octokit.rest.issues.addLabels({
+ labels,
+ owner: github.context.repo.owner,
+ repo: github.context.repo.repo,
+ issue_number: number,
+ });
+ } catch (e) {
+ if (e instanceof Error) {
+ core.error(e);
+ core.setFailed(e.message);
+ }
+ }
+}
+
+run();
diff --git a/.github/workflows/close-inactive-pr.yml b/.github/workflows/close-inactive-pr.yml
new file mode 100644
index 000000000..d5e0e9c95
--- /dev/null
+++ b/.github/workflows/close-inactive-pr.yml
@@ -0,0 +1,28 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: '0 0 * * *'
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v9
+ with:
+ days-before-issue-stale: 20
+ days-before-issue-close: -1
+ stale-issue-label: 'stale'
+ stale-issue-message: 'This issue is stale because it has been open for 15 days with no activity.'
+ exempt-issue-labels: 'long-term'
+ days-before-pr-stale: 20
+ days-before-pr-close: 7
+ stale-pr-label: 'stale'
+ stale-pr-message: 'This pull request is stale because it has been open for 15 days with no activity.'
+ close-pr-message: 'This pull request was closed because it has been inactive for 5 days since being marked as stale.'
+ only-pr-labels: 'answer'
+ exempt-pr-labels: 'challenge-creation, long-term, to be reviewed'
+ remove-pr-stale-when-updated: true
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/label-issue-update.yml b/.github/workflows/label-issue-update.yml
new file mode 100644
index 000000000..5f79b0568
--- /dev/null
+++ b/.github/workflows/label-issue-update.yml
@@ -0,0 +1,21 @@
+name: updates Labels
+
+on:
+ push:
+ branches-ignore:
+ - main
+
+jobs:
+ update_labels:
+ runs-on: ubuntu-latest
+ if: |
+ contains(github.event.pull_request.labels.*.name, 'sponsor') ||
+ contains(github.event.pull_request.labels.*.name, 'contributor')
+ steps:
+ - name: checkout
+ uses: actions/checkout@v2
+
+ - name: Add labels
+ uses: actions-ecosystem/action-add-labels@v1
+ with:
+ labels: to be reviewed/update
diff --git a/.github/workflows/label-issue.yml b/.github/workflows/label-issue.yml
new file mode 100644
index 000000000..a5fd40632
--- /dev/null
+++ b/.github/workflows/label-issue.yml
@@ -0,0 +1,37 @@
+name: Add Labels
+
+on:
+ pull_request_target:
+ types: [ opened, edited, synchronize ]
+
+jobs:
+ check-title:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check PR title
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ run: |
+ echo "Checking PR Title: '$PR_TITLE'"
+ if [[ ! "$PR_TITLE" =~ ^Answer: ]]; then
+ echo "❌ PR title should start with 'Answer:[#challenge number]'"
+ echo "### ❌ PR title should start with 'Answer:[#challenge number]'" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ else
+ echo "✅ PR title format is correct."
+ echo "### ✅ PR title format is correct." >> $GITHUB_STEP_SUMMARY
+ fi
+ add_labels:
+ runs-on: ubuntu-latest
+ if: ${{ startsWith(github.event.pull_request.title, 'Answer') }}
+ steps:
+ - name: checkout
+ uses: actions/checkout@v2
+
+ - name: Install dependencies
+ run: npm i @actions/core @actions/github
+
+ - name: Add labels
+ uses: ./.github/github-action/
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index b0decd9ac..0647ff22f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,6 @@ node_modules
# IDE - VSCode
.vscode/*
-!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
@@ -41,4 +40,9 @@ Thumbs.db
.angular
TODO.md
-.nx/cache
\ No newline at end of file
+.nx/cache
+.nx/workspace-data
+
+.cursorrules
+.cursor/rules/nx-rules.mdc
+.github/instructions/nx.instructions.md
diff --git a/.prettierignore b/.prettierignore
index ab639d33d..a4684ea74 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -5,4 +5,5 @@
.angular
-/.nx/cache
\ No newline at end of file
+/.nx/cache
+/.nx/workspace-data
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
index dea17d419..810823b36 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,4 +1,6 @@
{
"singleQuote": true,
- "bracketSameLine": true
+ "bracketSameLine": true,
+ "htmlWhitespaceSensitivity": "ignore",
+ "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1471fb96f..e0a129166 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,6 @@
{
- "eslint.validate": ["json"]
+ "eslint.validate": [
+ "json"
+ ],
+ "cSpell.language": "en,es-ES"
}
diff --git a/README.md b/README.md
index 747cd700d..2965ecc08 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ If you would like to propose a challenge, this project is open source, so feel f
## Challenges
-Check [all 39 challenges](https://angular-challenges.vercel.app/)
+Check [all 59 challenges](https://angular-challenges.vercel.app/)
## Contributors ✨
@@ -34,18 +34,48 @@ Check [all 39 challenges](https://angular-challenges.vercel.app/)
-  Laforge Thomas 🧩 💻 📖 🖋 🤔 🎨 |
+  Laforge Thomas 🧩 💻 📖 🖋 🤔 🎨 🇫🇷 |
+  Sven Brodny 📖 🧩 🖋 🎨 |
+  J. Degand 📖 🖋 💻 |
+  Devesh Chaudhari 💻 🐛 🧩 |
+  stillst 🧩 🇷🇺 |
+  Wandrille 🧩 |
+  Timothy Alcaide 🧩 |
+
+
+  Lance Finney 📖 🧩 |
+  Tsironis Ioannis 🧩 |
 Alan Dragicevich 📖 |
 Michel EDIGHOFFER 📖 |
 Gerardo Sebastian Gonzalez 📖 |
 Evseev Yuriy 🐛 |
 Tomer953 🐛 📖 💻 |
-  J. Degand 📖 |
-  Devesh Chaudhari 💻 🐛 🧩 |
 Dmitriy Mishchenko 📖 |
-  Sagar Devkota 📖 |
+  Sagar Devkota 📖 💻 |
+  Nelson Gutierrez 🇪🇸 |
+  Hossain K. M. 📖 |
+  Diogo Nishikawa 💻 🇵🇹 📖 |
+  Erick Rodriguez 🇪🇸 |
+  Eduardo Roth 📖 🇪🇸 |
+
+
+  Fernando Bello 📖 |
+  Лапин Андрей (Lapin Andrey) 🇷🇺 |
+  Dinar Shagaliev 🇷🇺 |
+  Vimulatus 📖 |
+  Arthur LANNELUCQ 🇫🇷 |
+  fixed_michal 🐛 |
+  Tenessy 🐛 |
+
+
+  Enoch Gao 📖 🇨🇳 |
+  Francisco Palma 🐛 |
+  Michał Grzegorczyk 📖 |
+  Tamim Arefin Anik 🐛 |
+  Matheus B. 🐛 |
+  Stef Heyenrath 📖 |
@@ -66,6 +96,8 @@ Check [all 39 challenges](https://angular-challenges.vercel.app/)
Contributions of any kind are welcome.
-## Licensev
+If I have forgotten to add you as a contributor, please reach out to me. 🙏
+
+## License
MIT
diff --git a/apps/angular/projection/.eslintrc.json b/apps/angular/1-projection/.eslintrc.json
similarity index 100%
rename from apps/angular/projection/.eslintrc.json
rename to apps/angular/1-projection/.eslintrc.json
diff --git a/apps/angular/1-projection/README.md b/apps/angular/1-projection/README.md
new file mode 100644
index 000000000..781198ead
--- /dev/null
+++ b/apps/angular/1-projection/README.md
@@ -0,0 +1,13 @@
+# Projection
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-projection
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/1-projection/).
diff --git a/apps/angular/1-projection/jest.config.ts b/apps/angular/1-projection/jest.config.ts
new file mode 100644
index 000000000..8eb2510a9
--- /dev/null
+++ b/apps/angular/1-projection/jest.config.ts
@@ -0,0 +1,23 @@
+/* eslint-disable */
+export default {
+ displayName: 'angular-projection',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ globals: {},
+ coverageDirectory: '../../../coverage/apps/angular/1-projection',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/apps/angular/1-projection/project.json b/apps/angular/1-projection/project.json
new file mode 100644
index 000000000..42ed1604f
--- /dev/null
+++ b/apps/angular/1-projection/project.json
@@ -0,0 +1,85 @@
+{
+ "name": "angular-projection",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/1-projection/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/1-projection",
+ "index": "apps/angular/1-projection/src/index.html",
+ "main": "apps/angular/1-projection/src/main.ts",
+ "polyfills": ["apps/angular/1-projection/src/polyfills.ts"],
+ "tsConfig": "apps/angular/1-projection/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/1-projection/src/favicon.ico",
+ "apps/angular/1-projection/src/assets"
+ ],
+ "styles": ["apps/angular/1-projection/src/styles.scss"],
+ "scripts": [],
+ "allowedCommonJsDependencies": ["seedrandom"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-projection:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-projection:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-projection:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/1-projection/src/app/app.component.ts b/apps/angular/1-projection/src/app/app.component.ts
new file mode 100644
index 000000000..df654bbc2
--- /dev/null
+++ b/apps/angular/1-projection/src/app/app.component.ts
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+import { CityCardComponent } from './component/city-card/city-card.component';
+import { StudentCardComponent } from './component/student-card/student-card.component';
+import { TeacherCardComponent } from './component/teacher-card/teacher-card.component';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+ `,
+ imports: [TeacherCardComponent, StudentCardComponent, CityCardComponent],
+})
+export class AppComponent {}
diff --git a/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts
new file mode 100644
index 000000000..8895c8c84
--- /dev/null
+++ b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts
@@ -0,0 +1,9 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+ selector: 'app-city-card',
+ template: 'TODO City',
+ imports: [],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CityCardComponent {}
diff --git a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts
new file mode 100644
index 000000000..bdfa4abd4
--- /dev/null
+++ b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts
@@ -0,0 +1,40 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ OnInit,
+} from '@angular/core';
+import { FakeHttpService } from '../../data-access/fake-http.service';
+import { StudentStore } from '../../data-access/student.store';
+import { CardType } from '../../model/card.model';
+import { CardComponent } from '../../ui/card/card.component';
+
+@Component({
+ selector: 'app-student-card',
+ template: `
+
+ `,
+ styles: [
+ `
+ ::ng-deep .bg-light-green {
+ background-color: rgba(0, 250, 0, 0.1);
+ }
+ `,
+ ],
+ imports: [CardComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class StudentCardComponent implements OnInit {
+ private http = inject(FakeHttpService);
+ private store = inject(StudentStore);
+
+ students = this.store.students;
+ cardType = CardType.STUDENT;
+
+ ngOnInit(): void {
+ this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
+ }
+}
diff --git a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts
new file mode 100644
index 000000000..adf0ad3c1
--- /dev/null
+++ b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts
@@ -0,0 +1,34 @@
+import { Component, inject, OnInit } from '@angular/core';
+import { FakeHttpService } from '../../data-access/fake-http.service';
+import { TeacherStore } from '../../data-access/teacher.store';
+import { CardType } from '../../model/card.model';
+import { CardComponent } from '../../ui/card/card.component';
+
+@Component({
+ selector: 'app-teacher-card',
+ template: `
+
+ `,
+ styles: [
+ `
+ ::ng-deep .bg-light-red {
+ background-color: rgba(250, 0, 0, 0.1);
+ }
+ `,
+ ],
+ imports: [CardComponent],
+})
+export class TeacherCardComponent implements OnInit {
+ private http = inject(FakeHttpService);
+ private store = inject(TeacherStore);
+
+ teachers = this.store.teachers;
+ cardType = CardType.TEACHER;
+
+ ngOnInit(): void {
+ this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
+ }
+}
diff --git a/apps/angular/1-projection/src/app/data-access/city.store.ts b/apps/angular/1-projection/src/app/data-access/city.store.ts
new file mode 100644
index 000000000..a8b523569
--- /dev/null
+++ b/apps/angular/1-projection/src/app/data-access/city.store.ts
@@ -0,0 +1,21 @@
+import { Injectable, signal } from '@angular/core';
+import { City } from '../model/city.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class CityStore {
+ private cities = signal([]);
+
+ addAll(cities: City[]) {
+ this.cities.set(cities);
+ }
+
+ addOne(city: City) {
+ this.cities.set([...this.cities(), city]);
+ }
+
+ deleteOne(id: number) {
+ this.cities.set(this.cities().filter((s) => s.id !== id));
+ }
+}
diff --git a/apps/angular/projection/src/app/data-access/fake-http.service.ts b/apps/angular/1-projection/src/app/data-access/fake-http.service.ts
similarity index 88%
rename from apps/angular/projection/src/app/data-access/fake-http.service.ts
rename to apps/angular/1-projection/src/app/data-access/fake-http.service.ts
index e21ce0336..82a8f1813 100644
--- a/apps/angular/projection/src/app/data-access/fake-http.service.ts
+++ b/apps/angular/1-projection/src/app/data-access/fake-http.service.ts
@@ -12,14 +12,14 @@ import {
import { map, timer } from 'rxjs';
import { City } from '../model/city.model';
import { Student } from '../model/student.model';
-import { subject, Teacher } from '../model/teacher.model';
+import { Teacher, subject } from '../model/teacher.model';
const factoryTeacher = incrementalNumber();
export const randTeacher = () => ({
id: factoryTeacher(),
- firstname: randFirstName(),
- lastname: randLastName(),
+ firstName: randFirstName(),
+ lastName: randLastName(),
subject: rand(subject),
});
@@ -34,8 +34,8 @@ const factoryStudent = incrementalNumber();
export const randStudent = (): Student => ({
id: factoryStudent(),
- firstname: randFirstName(),
- lastname: randLastName(),
+ firstName: randFirstName(),
+ lastName: randLastName(),
mainTeacher: teachers[randNumber({ max: teachers.length - 1 })],
school: randWord(),
});
diff --git a/apps/angular/1-projection/src/app/data-access/student.store.ts b/apps/angular/1-projection/src/app/data-access/student.store.ts
new file mode 100644
index 000000000..6e7f57022
--- /dev/null
+++ b/apps/angular/1-projection/src/app/data-access/student.store.ts
@@ -0,0 +1,21 @@
+import { Injectable, signal } from '@angular/core';
+import { Student } from '../model/student.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class StudentStore {
+ public students = signal([]);
+
+ addAll(students: Student[]) {
+ this.students.set(students);
+ }
+
+ addOne(student: Student) {
+ this.students.set([...this.students(), student]);
+ }
+
+ deleteOne(id: number) {
+ this.students.set(this.students().filter((s) => s.id !== id));
+ }
+}
diff --git a/apps/angular/1-projection/src/app/data-access/teacher.store.ts b/apps/angular/1-projection/src/app/data-access/teacher.store.ts
new file mode 100644
index 000000000..5f6dae989
--- /dev/null
+++ b/apps/angular/1-projection/src/app/data-access/teacher.store.ts
@@ -0,0 +1,21 @@
+import { Injectable, signal } from '@angular/core';
+import { Teacher } from '../model/teacher.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TeacherStore {
+ public teachers = signal([]);
+
+ addAll(teachers: Teacher[]) {
+ this.teachers.set(teachers);
+ }
+
+ addOne(teacher: Teacher) {
+ this.teachers.set([...this.teachers(), teacher]);
+ }
+
+ deleteOne(id: number) {
+ this.teachers.set(this.teachers().filter((t) => t.id !== id));
+ }
+}
diff --git a/apps/angular/projection/src/app/model/card.model.ts b/apps/angular/1-projection/src/app/model/card.model.ts
similarity index 100%
rename from apps/angular/projection/src/app/model/card.model.ts
rename to apps/angular/1-projection/src/app/model/card.model.ts
diff --git a/apps/angular/projection/src/app/model/city.model.ts b/apps/angular/1-projection/src/app/model/city.model.ts
similarity index 100%
rename from apps/angular/projection/src/app/model/city.model.ts
rename to apps/angular/1-projection/src/app/model/city.model.ts
diff --git a/apps/angular/1-projection/src/app/model/student.model.ts b/apps/angular/1-projection/src/app/model/student.model.ts
new file mode 100644
index 000000000..bc18e464a
--- /dev/null
+++ b/apps/angular/1-projection/src/app/model/student.model.ts
@@ -0,0 +1,9 @@
+import { Teacher } from './teacher.model';
+
+export interface Student {
+ id: number;
+ firstName: string;
+ lastName: string;
+ mainTeacher: Teacher;
+ school: string;
+}
diff --git a/apps/angular/1-projection/src/app/model/teacher.model.ts b/apps/angular/1-projection/src/app/model/teacher.model.ts
new file mode 100644
index 000000000..34b4241be
--- /dev/null
+++ b/apps/angular/1-projection/src/app/model/teacher.model.ts
@@ -0,0 +1,15 @@
+export const subject = [
+ 'Sciences',
+ 'History',
+ 'English',
+ 'Maths',
+ 'Sport',
+] as const;
+export type Subject = (typeof subject)[number];
+
+export interface Teacher {
+ id: number;
+ firstName: string;
+ lastName: string;
+ subject: Subject;
+}
diff --git a/apps/angular/1-projection/src/app/ui/card/card.component.ts b/apps/angular/1-projection/src/app/ui/card/card.component.ts
new file mode 100644
index 000000000..1a6c3648c
--- /dev/null
+++ b/apps/angular/1-projection/src/app/ui/card/card.component.ts
@@ -0,0 +1,58 @@
+import { NgOptimizedImage } from '@angular/common';
+import { Component, inject, input } from '@angular/core';
+import { randStudent, randTeacher } from '../../data-access/fake-http.service';
+import { StudentStore } from '../../data-access/student.store';
+import { TeacherStore } from '../../data-access/teacher.store';
+import { CardType } from '../../model/card.model';
+import { ListItemComponent } from '../list-item/list-item.component';
+
+@Component({
+ selector: 'app-card',
+ template: `
+
+ @if (type() === CardType.TEACHER) {
+
![]()
+ }
+ @if (type() === CardType.STUDENT) {
+
![]()
+ }
+
+
+ @for (item of list(); track item) {
+
+ }
+
+
+
+
+ `,
+ imports: [ListItemComponent, NgOptimizedImage],
+})
+export class CardComponent {
+ private teacherStore = inject(TeacherStore);
+ private studentStore = inject(StudentStore);
+
+ readonly list = input(null);
+ readonly type = input.required();
+ readonly customClass = input('');
+
+ CardType = CardType;
+
+ addNewItem() {
+ const type = this.type();
+ if (type === CardType.TEACHER) {
+ this.teacherStore.addOne(randTeacher());
+ } else if (type === CardType.STUDENT) {
+ this.studentStore.addOne(randStudent());
+ }
+ }
+}
diff --git a/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts
new file mode 100644
index 000000000..5d504f372
--- /dev/null
+++ b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts
@@ -0,0 +1,39 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ input,
+} from '@angular/core';
+import { StudentStore } from '../../data-access/student.store';
+import { TeacherStore } from '../../data-access/teacher.store';
+import { CardType } from '../../model/card.model';
+
+@Component({
+ selector: 'app-list-item',
+ template: `
+
+ {{ name() }}
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ListItemComponent {
+ private teacherStore = inject(TeacherStore);
+ private studentStore = inject(StudentStore);
+
+ readonly id = input.required();
+ readonly name = input.required();
+ readonly type = input.required();
+
+ delete(id: number) {
+ const type = this.type();
+ if (type === CardType.TEACHER) {
+ this.teacherStore.deleteOne(id);
+ } else if (type === CardType.STUDENT) {
+ this.studentStore.deleteOne(id);
+ }
+ }
+}
diff --git a/apps/angular/anchor-scrolling/src/assets/.gitkeep b/apps/angular/1-projection/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/anchor-scrolling/src/assets/.gitkeep
rename to apps/angular/1-projection/src/assets/.gitkeep
diff --git a/apps/angular/1-projection/src/assets/img/city.png b/apps/angular/1-projection/src/assets/img/city.png
new file mode 100644
index 000000000..c600f4455
Binary files /dev/null and b/apps/angular/1-projection/src/assets/img/city.png differ
diff --git a/apps/angular/projection/src/assets/img/student.webp b/apps/angular/1-projection/src/assets/img/student.webp
similarity index 100%
rename from apps/angular/projection/src/assets/img/student.webp
rename to apps/angular/1-projection/src/assets/img/student.webp
diff --git a/apps/angular/projection/src/assets/img/teacher.png b/apps/angular/1-projection/src/assets/img/teacher.png
similarity index 100%
rename from apps/angular/projection/src/assets/img/teacher.png
rename to apps/angular/1-projection/src/assets/img/teacher.png
diff --git a/apps/angular/projection/src/assets/svg/trash.svg b/apps/angular/1-projection/src/assets/svg/trash.svg
similarity index 100%
rename from apps/angular/projection/src/assets/svg/trash.svg
rename to apps/angular/1-projection/src/assets/svg/trash.svg
diff --git a/apps/angular/anchor-scrolling/src/favicon.ico b/apps/angular/1-projection/src/favicon.ico
similarity index 100%
rename from apps/angular/anchor-scrolling/src/favicon.ico
rename to apps/angular/1-projection/src/favicon.ico
diff --git a/apps/angular/1-projection/src/index.html b/apps/angular/1-projection/src/index.html
new file mode 100644
index 000000000..bb9c8bc84
--- /dev/null
+++ b/apps/angular/1-projection/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-projection
+
+
+
+
+
+
+
+
diff --git a/apps/angular/context-outlet-type/src/main.ts b/apps/angular/1-projection/src/main.ts
similarity index 100%
rename from apps/angular/context-outlet-type/src/main.ts
rename to apps/angular/1-projection/src/main.ts
diff --git a/apps/angular/context-outlet-type/src/polyfills.ts b/apps/angular/1-projection/src/polyfills.ts
similarity index 100%
rename from apps/angular/context-outlet-type/src/polyfills.ts
rename to apps/angular/1-projection/src/polyfills.ts
diff --git a/apps/angular/projection/src/styles.scss b/apps/angular/1-projection/src/styles.scss
similarity index 100%
rename from apps/angular/projection/src/styles.scss
rename to apps/angular/1-projection/src/styles.scss
diff --git a/apps/angular/anchor-scrolling/src/test-setup.ts b/apps/angular/1-projection/src/test-setup.ts
similarity index 100%
rename from apps/angular/anchor-scrolling/src/test-setup.ts
rename to apps/angular/1-projection/src/test-setup.ts
diff --git a/apps/angular/anchor-scrolling/tailwind.config.js b/apps/angular/1-projection/tailwind.config.js
similarity index 100%
rename from apps/angular/anchor-scrolling/tailwind.config.js
rename to apps/angular/1-projection/tailwind.config.js
diff --git a/apps/angular/1-projection/tsconfig.app.json b/apps/angular/1-projection/tsconfig.app.json
new file mode 100644
index 000000000..2a1ca1b8d
--- /dev/null
+++ b/apps/angular/1-projection/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "target": "ES2022",
+ "useDefineForClassFields": false,
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
+}
diff --git a/apps/angular/projection/tsconfig.editor.json b/apps/angular/1-projection/tsconfig.editor.json
similarity index 100%
rename from apps/angular/projection/tsconfig.editor.json
rename to apps/angular/1-projection/tsconfig.editor.json
diff --git a/apps/angular/crud/tsconfig.json b/apps/angular/1-projection/tsconfig.json
similarity index 100%
rename from apps/angular/crud/tsconfig.json
rename to apps/angular/1-projection/tsconfig.json
diff --git a/apps/angular/projection/tsconfig.spec.json b/apps/angular/1-projection/tsconfig.spec.json
similarity index 100%
rename from apps/angular/projection/tsconfig.spec.json
rename to apps/angular/1-projection/tsconfig.spec.json
diff --git a/apps/angular/crud/.eslintrc.json b/apps/angular/10-utility-wrapper-pipe/.eslintrc.json
similarity index 100%
rename from apps/angular/crud/.eslintrc.json
rename to apps/angular/10-utility-wrapper-pipe/.eslintrc.json
diff --git a/apps/angular/10-utility-wrapper-pipe/README.md b/apps/angular/10-utility-wrapper-pipe/README.md
new file mode 100644
index 000000000..aac426271
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/README.md
@@ -0,0 +1,13 @@
+# Utility Wrapper Pipe
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-utility-wrapper-pipe
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/10-pipe-utility/).
diff --git a/apps/angular/10-utility-wrapper-pipe/project.json b/apps/angular/10-utility-wrapper-pipe/project.json
new file mode 100644
index 000000000..37a204043
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/project.json
@@ -0,0 +1,73 @@
+{
+ "name": "angular-utility-wrapper-pipe",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/10-utility-wrapper-pipe/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/10-utility-wrapper-pipe",
+ "index": "apps/angular/10-utility-wrapper-pipe/src/index.html",
+ "main": "apps/angular/10-utility-wrapper-pipe/src/main.ts",
+ "polyfills": "apps/angular/10-utility-wrapper-pipe/src/polyfills.ts",
+ "tsConfig": "apps/angular/10-utility-wrapper-pipe/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/10-utility-wrapper-pipe/src/favicon.ico",
+ "apps/angular/10-utility-wrapper-pipe/src/assets"
+ ],
+ "styles": ["apps/angular/10-utility-wrapper-pipe/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-utility-wrapper-pipe:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-utility-wrapper-pipe:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-utility-wrapper-pipe:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/10-utility-wrapper-pipe/src/app/app.component.ts b/apps/angular/10-utility-wrapper-pipe/src/app/app.component.ts
new file mode 100644
index 000000000..9a8156a5f
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/src/app/app.component.ts
@@ -0,0 +1,37 @@
+import { Component } from '@angular/core';
+import { PersonUtils } from './person.utils';
+
+@Component({
+ selector: 'app-root',
+ template: `
+ @for (activity of activities; track activity.name) {
+ {{ activity.name }} :
+ @for (
+ person of persons;
+ track person.name;
+ let index = $index;
+ let isFirst = $first
+ ) {
+ {{ showName(person.name, index) }}
+ {{ isAllowed(person.age, isFirst, activity.minimumAge) }}
+ }
+ }
+ `,
+})
+export class AppComponent {
+ persons = [
+ { name: 'Toto', age: 10 },
+ { name: 'Jack', age: 15 },
+ { name: 'John', age: 30 },
+ ];
+
+ activities = [
+ { name: 'biking', minimumAge: 12 },
+ { name: 'hiking', minimumAge: 25 },
+ { name: 'dancing', minimumAge: 1 },
+ ];
+
+ showName = PersonUtils.showName;
+
+ isAllowed = PersonUtils.isAllowed;
+}
diff --git a/apps/angular/pipe-hard/src/app/person.utils.ts b/apps/angular/10-utility-wrapper-pipe/src/app/person.utils.ts
similarity index 100%
rename from apps/angular/pipe-hard/src/app/person.utils.ts
rename to apps/angular/10-utility-wrapper-pipe/src/app/person.utils.ts
diff --git a/apps/angular/bug-cd/src/assets/.gitkeep b/apps/angular/10-utility-wrapper-pipe/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/bug-cd/src/assets/.gitkeep
rename to apps/angular/10-utility-wrapper-pipe/src/assets/.gitkeep
diff --git a/apps/angular/bug-cd/src/favicon.ico b/apps/angular/10-utility-wrapper-pipe/src/favicon.ico
similarity index 100%
rename from apps/angular/bug-cd/src/favicon.ico
rename to apps/angular/10-utility-wrapper-pipe/src/favicon.ico
diff --git a/apps/angular/10-utility-wrapper-pipe/src/index.html b/apps/angular/10-utility-wrapper-pipe/src/index.html
new file mode 100644
index 000000000..01d31a163
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-utility-wrapper-pipe
+
+
+
+
+
+
+
+
diff --git a/apps/angular/decoupling/src/main.ts b/apps/angular/10-utility-wrapper-pipe/src/main.ts
similarity index 100%
rename from apps/angular/decoupling/src/main.ts
rename to apps/angular/10-utility-wrapper-pipe/src/main.ts
diff --git a/apps/angular/crud/src/polyfills.ts b/apps/angular/10-utility-wrapper-pipe/src/polyfills.ts
similarity index 100%
rename from apps/angular/crud/src/polyfills.ts
rename to apps/angular/10-utility-wrapper-pipe/src/polyfills.ts
diff --git a/apps/angular/context-outlet-type/src/styles.scss b/apps/angular/10-utility-wrapper-pipe/src/styles.scss
similarity index 100%
rename from apps/angular/context-outlet-type/src/styles.scss
rename to apps/angular/10-utility-wrapper-pipe/src/styles.scss
diff --git a/apps/angular/10-utility-wrapper-pipe/tsconfig.app.json b/apps/angular/10-utility-wrapper-pipe/tsconfig.app.json
new file mode 100644
index 000000000..2a1ca1b8d
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "target": "ES2022",
+ "useDefineForClassFields": false,
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
+}
diff --git a/apps/angular/context-outlet-type/tsconfig.editor.json b/apps/angular/10-utility-wrapper-pipe/tsconfig.editor.json
similarity index 100%
rename from apps/angular/context-outlet-type/tsconfig.editor.json
rename to apps/angular/10-utility-wrapper-pipe/tsconfig.editor.json
diff --git a/apps/angular/context-outlet-type/tsconfig.json b/apps/angular/10-utility-wrapper-pipe/tsconfig.json
similarity index 100%
rename from apps/angular/context-outlet-type/tsconfig.json
rename to apps/angular/10-utility-wrapper-pipe/tsconfig.json
diff --git a/apps/angular/anchor-scrolling/.eslintrc.json b/apps/angular/13-highly-customizable-css/.eslintrc.json
similarity index 100%
rename from apps/angular/anchor-scrolling/.eslintrc.json
rename to apps/angular/13-highly-customizable-css/.eslintrc.json
diff --git a/apps/angular/13-highly-customizable-css/README.md b/apps/angular/13-highly-customizable-css/README.md
new file mode 100644
index 000000000..d63171ae6
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/README.md
@@ -0,0 +1,13 @@
+# Highly Customizable CSS
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-highly-customizable-css
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/13-styling/).
diff --git a/apps/angular/13-highly-customizable-css/project.json b/apps/angular/13-highly-customizable-css/project.json
new file mode 100644
index 000000000..c20d3bb48
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/project.json
@@ -0,0 +1,73 @@
+{
+ "name": "angular-highly-customizable-css",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/13-highly-customizable-css/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/13-highly-customizable-css",
+ "index": "apps/angular/13-highly-customizable-css/src/index.html",
+ "main": "apps/angular/13-highly-customizable-css/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/13-highly-customizable-css/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/13-highly-customizable-css/src/favicon.ico",
+ "apps/angular/13-highly-customizable-css/src/assets"
+ ],
+ "styles": ["apps/angular/13-highly-customizable-css/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-highly-customizable-css:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-highly-customizable-css:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-highly-customizable-css:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/13-highly-customizable-css/src/app/page.component.ts b/apps/angular/13-highly-customizable-css/src/app/page.component.ts
new file mode 100644
index 000000000..029ca52d2
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/app/page.component.ts
@@ -0,0 +1,16 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component } from '@angular/core';
+import { TextStaticComponent } from './static-text.component';
+import { TextComponent } from './text.component';
+
+@Component({
+ selector: 'page',
+ imports: [TextStaticComponent, TextComponent],
+ template: `
+
+
+
+ This is a blue text
+ `,
+})
+export class PageComponent {}
diff --git a/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts b/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
new file mode 100644
index 000000000..703e2a538
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
@@ -0,0 +1,38 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component, computed, input } from '@angular/core';
+import { TextComponent } from './text.component';
+
+export type StaticTextType = 'normal' | 'warning' | 'error';
+
+@Component({
+ selector: 'static-text',
+ imports: [TextComponent],
+ template: `
+ This is a static text
+ `,
+})
+export class TextStaticComponent {
+ type = input('normal');
+
+ font = computed(() => {
+ switch (this.type()) {
+ case 'error':
+ return 30;
+ case 'warning':
+ return 25;
+ default:
+ return 10;
+ }
+ });
+
+ color = computed(() => {
+ switch (this.type()) {
+ case 'error':
+ return 'red';
+ case 'warning':
+ return 'orange';
+ default:
+ return 'black';
+ }
+ });
+}
diff --git a/apps/angular/13-highly-customizable-css/src/app/text.component.ts b/apps/angular/13-highly-customizable-css/src/app/text.component.ts
new file mode 100644
index 000000000..07e3e6255
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/app/text.component.ts
@@ -0,0 +1,15 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'text',
+ template: `
+
+
+
+ `,
+})
+export class TextComponent {
+ font = input(10);
+ color = input('black');
+}
diff --git a/apps/angular/crud/src/assets/.gitkeep b/apps/angular/13-highly-customizable-css/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/crud/src/assets/.gitkeep
rename to apps/angular/13-highly-customizable-css/src/assets/.gitkeep
diff --git a/apps/angular/context-outlet-type/src/favicon.ico b/apps/angular/13-highly-customizable-css/src/favicon.ico
similarity index 100%
rename from apps/angular/context-outlet-type/src/favicon.ico
rename to apps/angular/13-highly-customizable-css/src/favicon.ico
diff --git a/apps/angular/13-highly-customizable-css/src/index.html b/apps/angular/13-highly-customizable-css/src/index.html
new file mode 100644
index 000000000..e4a84b456
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-highly-customizable-css
+
+
+
+
+
+
+
+
diff --git a/apps/angular/styling/src/main.ts b/apps/angular/13-highly-customizable-css/src/main.ts
similarity index 100%
rename from apps/angular/styling/src/main.ts
rename to apps/angular/13-highly-customizable-css/src/main.ts
diff --git a/apps/angular/ngfor-enhancement/src/styles.scss b/apps/angular/13-highly-customizable-css/src/styles.scss
similarity index 100%
rename from apps/angular/ngfor-enhancement/src/styles.scss
rename to apps/angular/13-highly-customizable-css/src/styles.scss
diff --git a/apps/angular/13-highly-customizable-css/tsconfig.app.json b/apps/angular/13-highly-customizable-css/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/di/tsconfig.editor.json b/apps/angular/13-highly-customizable-css/tsconfig.editor.json
similarity index 100%
rename from apps/angular/di/tsconfig.editor.json
rename to apps/angular/13-highly-customizable-css/tsconfig.editor.json
diff --git a/apps/angular/decoupling/tsconfig.json b/apps/angular/13-highly-customizable-css/tsconfig.json
similarity index 100%
rename from apps/angular/decoupling/tsconfig.json
rename to apps/angular/13-highly-customizable-css/tsconfig.json
diff --git a/apps/angular/di/.eslintrc.json b/apps/angular/16-master-dependency-injection/.eslintrc.json
similarity index 100%
rename from apps/angular/di/.eslintrc.json
rename to apps/angular/16-master-dependency-injection/.eslintrc.json
diff --git a/apps/angular/16-master-dependency-injection/README.md b/apps/angular/16-master-dependency-injection/README.md
new file mode 100644
index 000000000..be19c1ba3
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/README.md
@@ -0,0 +1,13 @@
+# Master Dependancy Injection
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-master-dependency-injection
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/16-di/).
diff --git a/apps/angular/16-master-dependency-injection/project.json b/apps/angular/16-master-dependency-injection/project.json
new file mode 100644
index 000000000..4eb6bd95e
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/project.json
@@ -0,0 +1,75 @@
+{
+ "name": "angular-master-dependency-injection",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/16-master-dependency-injection/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/16-master-dependency-injection",
+ "index": "apps/angular/16-master-dependency-injection/src/index.html",
+ "main": "apps/angular/16-master-dependency-injection/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/16-master-dependency-injection/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/16-master-dependency-injection/src/favicon.ico",
+ "apps/angular/16-master-dependency-injection/src/assets"
+ ],
+ "styles": [
+ "apps/angular/16-master-dependency-injection/src/styles.scss"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-master-dependency-injection:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-master-dependency-injection:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-master-dependency-injection:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/app.component.ts b/apps/angular/16-master-dependency-injection/src/app/app.component.ts
new file mode 100644
index 000000000..332ec9877
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/app.component.ts
@@ -0,0 +1,54 @@
+import { TableComponent } from '@angular-challenges/shared/ui';
+import { AsyncPipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, Directive } from '@angular/core';
+import { CurrencyPipe } from './currency.pipe';
+import { CurrencyService } from './currency.service';
+import { Product, products } from './product.model';
+
+interface ProductContext {
+ $implicit: Product;
+}
+
+@Directive({
+ selector: 'ng-template[product]',
+})
+export class ProductDirective {
+ static ngTemplateContextGuard(
+ dir: ProductDirective,
+ ctx: unknown,
+ ): ctx is ProductContext {
+ return true;
+ }
+}
+
+@Component({
+ imports: [TableComponent, CurrencyPipe, AsyncPipe, ProductDirective],
+ providers: [CurrencyService],
+ selector: 'app-root',
+ template: `
+
+
+
+ @for (col of displayedColumns; track $index) {
+
+ {{ col }}
+ |
+ }
+
+
+
+
+ {{ product.name }} |
+ {{ product.priceA | currency | async }} |
+ {{ product.priceB | currency | async }} |
+ {{ product.priceC | currency | async }} |
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {
+ products = products;
+ displayedColumns = ['name', 'priceA', 'priceB', 'priceC'];
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/currency.pipe.ts b/apps/angular/16-master-dependency-injection/src/app/currency.pipe.ts
new file mode 100644
index 000000000..efa408eb8
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/currency.pipe.ts
@@ -0,0 +1,16 @@
+import { inject, Pipe, PipeTransform } from '@angular/core';
+import { map } from 'rxjs';
+import { CurrencyService } from './currency.service';
+
+@Pipe({
+ name: 'currency',
+})
+export class CurrencyPipe implements PipeTransform {
+ currencyService = inject(CurrencyService);
+
+ transform(price: number) {
+ return this.currencyService.symbol$.pipe(
+ map((s) => `${String(price)}${s}`),
+ );
+ }
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/currency.service.ts b/apps/angular/16-master-dependency-injection/src/app/currency.service.ts
new file mode 100644
index 000000000..38b403e48
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/currency.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import { ComponentStore } from '@ngrx/component-store';
+import { map } from 'rxjs';
+
+export interface Currency {
+ name: string;
+ code: string;
+ symbol: string;
+}
+
+export const currency: Currency[] = [
+ { name: 'Euro', code: 'EUR', symbol: '€' },
+ { name: 'Dollar US', code: 'USD', symbol: 'US$' },
+ { name: 'Dollar Autralien', code: 'AUD', symbol: 'AU$' },
+ { name: 'Livre Sterling', code: 'GBP', symbol: '£' },
+ { name: 'Dollar Canadien', code: 'CAD', symbol: 'CAD' },
+];
+
+@Injectable()
+export class CurrencyService extends ComponentStore<{ code: string }> {
+ readonly code$ = this.select((state) => state.code);
+ readonly symbol$ = this.code$.pipe(
+ map((code) => currency.find((c) => c.code === code)?.symbol ?? code),
+ );
+
+ constructor() {
+ super({ code: 'EUR' });
+ }
+}
diff --git a/apps/angular/di/src/app/product.model.ts b/apps/angular/16-master-dependency-injection/src/app/product.model.ts
similarity index 100%
rename from apps/angular/di/src/app/product.model.ts
rename to apps/angular/16-master-dependency-injection/src/app/product.model.ts
diff --git a/apps/angular/decoupling/src/assets/.gitkeep b/apps/angular/16-master-dependency-injection/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/decoupling/src/assets/.gitkeep
rename to apps/angular/16-master-dependency-injection/src/assets/.gitkeep
diff --git a/apps/angular/crud/src/favicon.ico b/apps/angular/16-master-dependency-injection/src/favicon.ico
similarity index 100%
rename from apps/angular/crud/src/favicon.ico
rename to apps/angular/16-master-dependency-injection/src/favicon.ico
diff --git a/apps/angular/16-master-dependency-injection/src/index.html b/apps/angular/16-master-dependency-injection/src/index.html
new file mode 100644
index 000000000..be35bf8c8
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-master-dependancy-injection
+
+
+
+
+
+
+
+
diff --git a/apps/angular/di/src/main.ts b/apps/angular/16-master-dependency-injection/src/main.ts
similarity index 100%
rename from apps/angular/di/src/main.ts
rename to apps/angular/16-master-dependency-injection/src/main.ts
diff --git a/apps/angular/di/src/styles.scss b/apps/angular/16-master-dependency-injection/src/styles.scss
similarity index 100%
rename from apps/angular/di/src/styles.scss
rename to apps/angular/16-master-dependency-injection/src/styles.scss
diff --git a/apps/angular/16-master-dependency-injection/tsconfig.app.json b/apps/angular/16-master-dependency-injection/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/router-input/tsconfig.editor.json b/apps/angular/16-master-dependency-injection/tsconfig.editor.json
similarity index 100%
rename from apps/angular/router-input/tsconfig.editor.json
rename to apps/angular/16-master-dependency-injection/tsconfig.editor.json
diff --git a/apps/angular/di/tsconfig.json b/apps/angular/16-master-dependency-injection/tsconfig.json
similarity index 100%
rename from apps/angular/di/tsconfig.json
rename to apps/angular/16-master-dependency-injection/tsconfig.json
diff --git a/apps/angular/bug-cd/.eslintrc.json b/apps/angular/21-anchor-navigation/.eslintrc.json
similarity index 100%
rename from apps/angular/bug-cd/.eslintrc.json
rename to apps/angular/21-anchor-navigation/.eslintrc.json
diff --git a/apps/angular/21-anchor-navigation/README.md b/apps/angular/21-anchor-navigation/README.md
new file mode 100644
index 000000000..3683899ba
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/README.md
@@ -0,0 +1,13 @@
+# Anchor Navigation
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-anchor-navigation
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/21-achor-scrolling/).
diff --git a/apps/angular/21-anchor-navigation/jest.config.ts b/apps/angular/21-anchor-navigation/jest.config.ts
new file mode 100644
index 000000000..7347f70b5
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'anchor-navigation-anchor-navigation',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/21-anchor-navigation',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/apps/angular/21-anchor-navigation/project.json b/apps/angular/21-anchor-navigation/project.json
new file mode 100644
index 000000000..782bb1ec4
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/project.json
@@ -0,0 +1,84 @@
+{
+ "name": "angular-anchor-navigation",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/21-anchor-navigation/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/21-anchor-navigation",
+ "index": "apps/angular/21-anchor-navigation/src/index.html",
+ "main": "apps/angular/21-anchor-navigation/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/21-anchor-navigation/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/21-anchor-navigation/src/favicon.ico",
+ "apps/angular/21-anchor-navigation/src/assets"
+ ],
+ "styles": ["apps/angular/21-anchor-navigation/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-anchor-navigation:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-anchor-navigation:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-anchor-navigation:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/21-anchor-navigation/src/app/app.component.ts b/apps/angular/21-anchor-navigation/src/app/app.component.ts
new file mode 100644
index 000000000..5caca0271
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+
+ `,
+})
+export class AppComponent {}
diff --git a/apps/angular/anchor-scrolling/src/app/app.config.ts b/apps/angular/21-anchor-navigation/src/app/app.config.ts
similarity index 100%
rename from apps/angular/anchor-scrolling/src/app/app.config.ts
rename to apps/angular/21-anchor-navigation/src/app/app.config.ts
diff --git a/apps/angular/anchor-scrolling/src/app/app.routes.ts b/apps/angular/21-anchor-navigation/src/app/app.routes.ts
similarity index 100%
rename from apps/angular/anchor-scrolling/src/app/app.routes.ts
rename to apps/angular/21-anchor-navigation/src/app/app.routes.ts
diff --git a/apps/angular/21-anchor-navigation/src/app/foo.component.ts b/apps/angular/21-anchor-navigation/src/app/foo.component.ts
new file mode 100644
index 000000000..6744c3662
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/foo.component.ts
@@ -0,0 +1,14 @@
+import { Component } from '@angular/core';
+import { NavButtonComponent } from './nav-button.component';
+
+@Component({
+ imports: [NavButtonComponent],
+ selector: 'app-foo',
+ template: `
+ Welcome to foo page
+ Home Page
+ section 1
+ section 2
+ `,
+})
+export class FooComponent {}
diff --git a/apps/angular/21-anchor-navigation/src/app/home.component.ts b/apps/angular/21-anchor-navigation/src/app/home.component.ts
new file mode 100644
index 000000000..6ef9bc2b6
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/home.component.ts
@@ -0,0 +1,19 @@
+import { Component } from '@angular/core';
+import { NavButtonComponent } from './nav-button.component';
+
+@Component({
+ imports: [NavButtonComponent],
+ selector: 'app-home',
+ template: `
+ Foo Page
+
+ Empty
+ Scroll Bottom
+
+
+ I want to scroll each
+ Scroll Top
+
+ `,
+})
+export class HomeComponent {}
diff --git a/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts b/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
new file mode 100644
index 000000000..7a22c7f38
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
@@ -0,0 +1,17 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'nav-button',
+ template: `
+
+
+
+ `,
+ host: {
+ class: 'block w-fit border border-red-500 rounded-md p-4 m-2',
+ },
+})
+export class NavButtonComponent {
+ href = input('');
+}
diff --git a/apps/angular/di/src/assets/.gitkeep b/apps/angular/21-anchor-navigation/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/di/src/assets/.gitkeep
rename to apps/angular/21-anchor-navigation/src/assets/.gitkeep
diff --git a/apps/angular/decoupling/src/favicon.ico b/apps/angular/21-anchor-navigation/src/favicon.ico
similarity index 100%
rename from apps/angular/decoupling/src/favicon.ico
rename to apps/angular/21-anchor-navigation/src/favicon.ico
diff --git a/apps/angular/21-anchor-navigation/src/index.html b/apps/angular/21-anchor-navigation/src/index.html
new file mode 100644
index 000000000..06a706a0a
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-anchor-navigation
+
+
+
+
+
+
+
+
diff --git a/apps/angular/21-anchor-navigation/src/main.ts b/apps/angular/21-anchor-navigation/src/main.ts
new file mode 100644
index 000000000..7961924bf
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/main.ts
@@ -0,0 +1,8 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, appConfig).catch((err) =>
+ console.error(err),
+);
diff --git a/apps/angular/anchor-scrolling/src/styles.scss b/apps/angular/21-anchor-navigation/src/styles.scss
similarity index 100%
rename from apps/angular/anchor-scrolling/src/styles.scss
rename to apps/angular/21-anchor-navigation/src/styles.scss
diff --git a/apps/angular/projection/src/test-setup.ts b/apps/angular/21-anchor-navigation/src/test-setup.ts
similarity index 100%
rename from apps/angular/projection/src/test-setup.ts
rename to apps/angular/21-anchor-navigation/src/test-setup.ts
diff --git a/apps/angular/bug-cd/tailwind.config.js b/apps/angular/21-anchor-navigation/tailwind.config.js
similarity index 100%
rename from apps/angular/bug-cd/tailwind.config.js
rename to apps/angular/21-anchor-navigation/tailwind.config.js
diff --git a/apps/angular/21-anchor-navigation/tsconfig.app.json b/apps/angular/21-anchor-navigation/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/anchor-scrolling/tsconfig.editor.json b/apps/angular/21-anchor-navigation/tsconfig.editor.json
similarity index 100%
rename from apps/angular/anchor-scrolling/tsconfig.editor.json
rename to apps/angular/21-anchor-navigation/tsconfig.editor.json
diff --git a/apps/angular/anchor-scrolling/tsconfig.json b/apps/angular/21-anchor-navigation/tsconfig.json
similarity index 100%
rename from apps/angular/anchor-scrolling/tsconfig.json
rename to apps/angular/21-anchor-navigation/tsconfig.json
diff --git a/apps/angular/anchor-scrolling/tsconfig.spec.json b/apps/angular/21-anchor-navigation/tsconfig.spec.json
similarity index 100%
rename from apps/angular/anchor-scrolling/tsconfig.spec.json
rename to apps/angular/21-anchor-navigation/tsconfig.spec.json
diff --git a/apps/angular/decoupling/.eslintrc.json b/apps/angular/22-router-input/.eslintrc.json
similarity index 100%
rename from apps/angular/decoupling/.eslintrc.json
rename to apps/angular/22-router-input/.eslintrc.json
diff --git a/apps/angular/22-router-input/README.md b/apps/angular/22-router-input/README.md
new file mode 100644
index 000000000..0aad6c326
--- /dev/null
+++ b/apps/angular/22-router-input/README.md
@@ -0,0 +1,13 @@
+# @RouterInput()
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-router-input
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/22-router-input/).
diff --git a/apps/angular/22-router-input/project.json b/apps/angular/22-router-input/project.json
new file mode 100644
index 000000000..d0cd43a08
--- /dev/null
+++ b/apps/angular/22-router-input/project.json
@@ -0,0 +1,72 @@
+{
+ "name": "angular-router-input",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/22-router-input/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/22-router-input",
+ "index": "apps/angular/22-router-input/src/index.html",
+ "main": "apps/angular/22-router-input/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/22-router-input/tsconfig.app.json",
+ "assets": [
+ "apps/angular/22-router-input/src/favicon.ico",
+ "apps/angular/22-router-input/src/assets"
+ ],
+ "styles": ["apps/angular/22-router-input/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-router-input:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-router-input:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-router-input:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/22-router-input/src/app/app.component.ts b/apps/angular/22-router-input/src/app/app.component.ts
new file mode 100644
index 000000000..9dfc11200
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/app.component.ts
@@ -0,0 +1,25 @@
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { RouterLink, RouterModule } from '@angular/router';
+
+@Component({
+ imports: [RouterLink, RouterModule, ReactiveFormsModule],
+ selector: 'app-root',
+ template: `
+
+
+
+
+
+
+
+ `,
+})
+export class AppComponent {
+ userName = new FormControl();
+ testId = new FormControl();
+}
diff --git a/apps/angular/router-input/src/app/app.config.ts b/apps/angular/22-router-input/src/app/app.config.ts
similarity index 100%
rename from apps/angular/router-input/src/app/app.config.ts
rename to apps/angular/22-router-input/src/app/app.config.ts
diff --git a/apps/angular/22-router-input/src/app/app.routes.ts b/apps/angular/22-router-input/src/app/app.routes.ts
new file mode 100644
index 000000000..f5d3487c4
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/app.routes.ts
@@ -0,0 +1,15 @@
+import { Route } from '@angular/router';
+
+export const appRoutes: Route[] = [
+ {
+ path: '',
+ loadComponent: () => import('./home.component'),
+ },
+ {
+ path: 'subscription/:testId',
+ loadComponent: () => import('./test.component'),
+ data: {
+ permission: 'admin',
+ },
+ },
+];
diff --git a/apps/angular/22-router-input/src/app/home.component.ts b/apps/angular/22-router-input/src/app/home.component.ts
new file mode 100644
index 000000000..0ddc1501d
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/home.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+@Component({
+ selector: 'app-home',
+ imports: [],
+ template: `
+ Home
+ `,
+})
+export default class HomeComponent {}
diff --git a/apps/angular/router-input/src/app/test.component.ts b/apps/angular/22-router-input/src/app/test.component.ts
similarity index 93%
rename from apps/angular/router-input/src/app/test.component.ts
rename to apps/angular/22-router-input/src/app/test.component.ts
index 56ac3f722..747ab4483 100644
--- a/apps/angular/router-input/src/app/test.component.ts
+++ b/apps/angular/22-router-input/src/app/test.component.ts
@@ -2,9 +2,9 @@ import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';
+
@Component({
- selector: 'app-test',
- standalone: true,
+ selector: 'app-subscription',
imports: [AsyncPipe],
template: `
TestId: {{ testId$ | async }}
diff --git a/apps/angular/injection-token/src/assets/.gitkeep b/apps/angular/22-router-input/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/injection-token/src/assets/.gitkeep
rename to apps/angular/22-router-input/src/assets/.gitkeep
diff --git a/apps/angular/di/src/favicon.ico b/apps/angular/22-router-input/src/favicon.ico
similarity index 100%
rename from apps/angular/di/src/favicon.ico
rename to apps/angular/22-router-input/src/favicon.ico
diff --git a/apps/angular/22-router-input/src/index.html b/apps/angular/22-router-input/src/index.html
new file mode 100644
index 000000000..30e74f7b4
--- /dev/null
+++ b/apps/angular/22-router-input/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-router-input
+
+
+
+
+
+
+
+
diff --git a/apps/angular/22-router-input/src/main.ts b/apps/angular/22-router-input/src/main.ts
new file mode 100644
index 000000000..f3a7223da
--- /dev/null
+++ b/apps/angular/22-router-input/src/main.ts
@@ -0,0 +1,7 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, appConfig).catch((err) =>
+ console.error(err),
+);
diff --git a/apps/angular/pipe-easy/src/styles.scss b/apps/angular/22-router-input/src/styles.scss
similarity index 100%
rename from apps/angular/pipe-easy/src/styles.scss
rename to apps/angular/22-router-input/src/styles.scss
diff --git a/apps/angular/22-router-input/tsconfig.app.json b/apps/angular/22-router-input/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/22-router-input/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/styling/tsconfig.editor.json b/apps/angular/22-router-input/tsconfig.editor.json
similarity index 100%
rename from apps/angular/styling/tsconfig.editor.json
rename to apps/angular/22-router-input/tsconfig.editor.json
diff --git a/apps/angular/module-to-standalone/tsconfig.json b/apps/angular/22-router-input/tsconfig.json
similarity index 100%
rename from apps/angular/module-to-standalone/tsconfig.json
rename to apps/angular/22-router-input/tsconfig.json
diff --git a/apps/angular/interop-rxjs-signal/.eslintrc.json b/apps/angular/31-module-to-standalone/.eslintrc.json
similarity index 100%
rename from apps/angular/interop-rxjs-signal/.eslintrc.json
rename to apps/angular/31-module-to-standalone/.eslintrc.json
diff --git a/apps/angular/31-module-to-standalone/README.md b/apps/angular/31-module-to-standalone/README.md
new file mode 100644
index 000000000..bd227b7a2
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/README.md
@@ -0,0 +1,13 @@
+# Module to Standalone
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-module-to-standalone
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/31-module-to-standalone/).
diff --git a/apps/angular/31-module-to-standalone/project.json b/apps/angular/31-module-to-standalone/project.json
new file mode 100644
index 000000000..b02e0a0a8
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/project.json
@@ -0,0 +1,72 @@
+{
+ "name": "angular-module-to-standalone",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/31-module-to-standalone/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/31-module-to-standalone",
+ "index": "apps/angular/31-module-to-standalone/src/index.html",
+ "main": "apps/angular/31-module-to-standalone/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/31-module-to-standalone/tsconfig.app.json",
+ "assets": [
+ "apps/angular/31-module-to-standalone/src/favicon.ico",
+ "apps/angular/31-module-to-standalone/src/assets"
+ ],
+ "styles": ["apps/angular/31-module-to-standalone/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-module-to-standalone:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-module-to-standalone:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-module-to-standalone:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/31-module-to-standalone/src/app/app.component.ts b/apps/angular/31-module-to-standalone/src/app/app.component.ts
new file mode 100644
index 000000000..986df84b5
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/src/app/app.component.ts
@@ -0,0 +1,30 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+
+
+
+
+
+ `,
+ host: {
+ class: 'flex flex-col p-4 gap-3',
+ },
+ standalone: false,
+})
+export class AppComponent {}
diff --git a/apps/angular/module-to-standalone/src/app/app.module.ts b/apps/angular/31-module-to-standalone/src/app/app.module.ts
similarity index 100%
rename from apps/angular/module-to-standalone/src/app/app.module.ts
rename to apps/angular/31-module-to-standalone/src/app/app.module.ts
diff --git a/apps/angular/interop-rxjs-signal/src/assets/.gitkeep b/apps/angular/31-module-to-standalone/src/assets/.gitkeep
similarity index 100%
rename from apps/angular/interop-rxjs-signal/src/assets/.gitkeep
rename to apps/angular/31-module-to-standalone/src/assets/.gitkeep
diff --git a/apps/angular/injection-token/src/favicon.ico b/apps/angular/31-module-to-standalone/src/favicon.ico
similarity index 100%
rename from apps/angular/injection-token/src/favicon.ico
rename to apps/angular/31-module-to-standalone/src/favicon.ico
diff --git a/apps/angular/31-module-to-standalone/src/index.html b/apps/angular/31-module-to-standalone/src/index.html
new file mode 100644
index 000000000..fe0d5b978
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-module-to-standalone
+
+
+
+
+
+
+
+
diff --git a/apps/angular/module-to-standalone/src/main.ts b/apps/angular/31-module-to-standalone/src/main.ts
similarity index 100%
rename from apps/angular/module-to-standalone/src/main.ts
rename to apps/angular/31-module-to-standalone/src/main.ts
diff --git a/apps/angular/bug-cd/src/styles.scss b/apps/angular/31-module-to-standalone/src/styles.scss
similarity index 100%
rename from apps/angular/bug-cd/src/styles.scss
rename to apps/angular/31-module-to-standalone/src/styles.scss
diff --git a/apps/angular/module-to-standalone/tailwind.config.js b/apps/angular/31-module-to-standalone/tailwind.config.js
similarity index 100%
rename from apps/angular/module-to-standalone/tailwind.config.js
rename to apps/angular/31-module-to-standalone/tailwind.config.js
diff --git a/apps/angular/31-module-to-standalone/tsconfig.app.json b/apps/angular/31-module-to-standalone/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/module-to-standalone/tsconfig.editor.json b/apps/angular/31-module-to-standalone/tsconfig.editor.json
similarity index 100%
rename from apps/angular/module-to-standalone/tsconfig.editor.json
rename to apps/angular/31-module-to-standalone/tsconfig.editor.json
diff --git a/apps/angular/router-input/tsconfig.json b/apps/angular/31-module-to-standalone/tsconfig.json
similarity index 100%
rename from apps/angular/router-input/tsconfig.json
rename to apps/angular/31-module-to-standalone/tsconfig.json
diff --git a/apps/angular/module-to-standalone/.eslintrc.json b/apps/angular/32-change-detection-bug/.eslintrc.json
similarity index 100%
rename from apps/angular/module-to-standalone/.eslintrc.json
rename to apps/angular/32-change-detection-bug/.eslintrc.json
diff --git a/apps/angular/32-change-detection-bug/README.md b/apps/angular/32-change-detection-bug/README.md
new file mode 100644
index 000000000..41e533388
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/README.md
@@ -0,0 +1,13 @@
+# Change Detection Bug
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-change-detection-bug
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/performance/32-bug-cd/).
diff --git a/apps/angular/32-change-detection-bug/jest.config.ts b/apps/angular/32-change-detection-bug/jest.config.ts
new file mode 100644
index 000000000..d0412f028
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'angular-change-detection-bug',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/32-change-detection-bug',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/apps/angular/32-change-detection-bug/project.json b/apps/angular/32-change-detection-bug/project.json
new file mode 100644
index 000000000..977b76334
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/project.json
@@ -0,0 +1,83 @@
+{
+ "name": "angular-change-detection-bug",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/32-change-detection-bug/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/32-change-detection-bug",
+ "index": "apps/angular/32-change-detection-bug/src/index.html",
+ "main": "apps/angular/32-change-detection-bug/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/32-change-detection-bug/tsconfig.app.json",
+ "assets": [
+ "apps/angular/32-change-detection-bug/src/favicon.ico",
+ "apps/angular/32-change-detection-bug/src/assets"
+ ],
+ "styles": ["apps/angular/32-change-detection-bug/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-change-detection-bug:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-change-detection-bug:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-change-detection-bug:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/32-change-detection-bug/src/app/app.component.ts b/apps/angular/32-change-detection-bug/src/app/app.component.ts
new file mode 100644
index 000000000..217999c3a
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/app.component.ts
@@ -0,0 +1,20 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+ My Application
+
+ `,
+ host: {
+ class: 'flex flex-col gap-2',
+ },
+})
+export class AppComponent {}
diff --git a/apps/angular/bug-cd/src/app/app.config.ts b/apps/angular/32-change-detection-bug/src/app/app.config.ts
similarity index 100%
rename from apps/angular/bug-cd/src/app/app.config.ts
rename to apps/angular/32-change-detection-bug/src/app/app.config.ts
diff --git a/apps/angular/32-change-detection-bug/src/app/bar.component.ts b/apps/angular/32-change-detection-bug/src/app/bar.component.ts
new file mode 100644
index 000000000..81981f99d
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/bar.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-bar',
+ template: `
+ BarComponent
+ `,
+})
+export class BarComponent {}
diff --git a/apps/angular/bug-cd/src/app/fake.service.ts b/apps/angular/32-change-detection-bug/src/app/fake.service.ts
similarity index 100%
rename from apps/angular/bug-cd/src/app/fake.service.ts
rename to apps/angular/32-change-detection-bug/src/app/fake.service.ts
diff --git a/apps/angular/32-change-detection-bug/src/app/foo.component.ts b/apps/angular/32-change-detection-bug/src/app/foo.component.ts
new file mode 100644
index 000000000..1fcb24326
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/foo.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-foo',
+ template: `
+ Foo Component
+ `,
+})
+export class FooComponent {}
diff --git a/apps/angular/32-change-detection-bug/src/app/main-navigation.component.ts b/apps/angular/32-change-detection-bug/src/app/main-navigation.component.ts
new file mode 100644
index 000000000..1a3a5e93a
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/main-navigation.component.ts
@@ -0,0 +1,63 @@
+import { Component, inject, input } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { RouterLink, RouterLinkActive } from '@angular/router';
+import { FakeServiceService } from './fake.service';
+
+interface MenuItem {
+ path: string;
+ name: string;
+}
+
+@Component({
+ selector: 'app-nav',
+ imports: [RouterLink, RouterLinkActive],
+ template: `
+ @for (menu of menus(); track menu.path) {
+
+ {{ menu.name }}
+
+ }
+ `,
+ styles: [
+ `
+ a.isSelected {
+ @apply bg-gray-600 text-white;
+ }
+ `,
+ ],
+ host: {
+ class: 'flex flex-col p-2 gap-2',
+ },
+})
+export class NavigationComponent {
+ menus = input.required