mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 12:53:03 -05:00
Merge branch 'main' into all-contributors/add-kabrunko-dev
This commit is contained in:
@@ -26,9 +26,9 @@
|
||||
"symbol": "🇵🇹",
|
||||
"description": "Translate in Portuguese"
|
||||
},
|
||||
"translation-pt-BR": {
|
||||
"symbol": "🇧🇷",
|
||||
"description": "Translate in Brazilian Portuguese"
|
||||
"translation-ru": {
|
||||
"symbol": "🇷🇺",
|
||||
"description": "Translate in Russian"
|
||||
}
|
||||
},
|
||||
"contributors": [
|
||||
@@ -47,6 +47,38 @@
|
||||
"translation-fr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "alan-bio",
|
||||
"name": "Alan Dragicevich",
|
||||
@@ -94,26 +126,6 @@
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jdegand",
|
||||
"name": "J. Degand",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
|
||||
"profile": "https://github.com/jdegand",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "dmmishchenko",
|
||||
"name": "Dmitriy Mishchenko",
|
||||
@@ -157,9 +169,28 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/142346548?v=4",
|
||||
"profile": "https://github.com/kabrunko-dev/",
|
||||
"contributions": [
|
||||
"translation-pt-BR",
|
||||
"code",
|
||||
"translation-pt",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"code"
|
||||
"translation-es"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
2
.github/workflows/close-inactive-pr.yml
vendored
2
.github/workflows/close-inactive-pr.yml
vendored
@@ -16,10 +16,12 @@ jobs:
|
||||
days-before-issue-close: -1
|
||||
stale-issue-label: 'stale'
|
||||
stale-issue-message: 'This issue is stale because it has been open for 20 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 20 days with no activity.'
|
||||
close-pr-message: 'This pull request was closed because it has been inactive for 7 days since being marked as stale.'
|
||||
only-pr-labels: 'answer'
|
||||
exempt-pr-labels: 'challenge-creation, long-term'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,7 +19,6 @@ node_modules
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"eslint.validate": ["json"]
|
||||
"eslint.validate": [
|
||||
"json"
|
||||
],
|
||||
"cSpell.language": "en,es-ES"
|
||||
}
|
||||
|
||||
17
README.md
17
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 40 challenges](https://angular-challenges.vercel.app/)
|
||||
Check [all 43 challenges](https://angular-challenges.vercel.app/)
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
@@ -35,20 +35,25 @@ Check [all 40 challenges](https://angular-challenges.vercel.app/)
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thomaslaforge.dev/home"><img src="https://avatars.githubusercontent.com/u/30832608?v=4?s=100" width="100px;" alt="Laforge Thomas"/><br /><sub><b>Laforge Thomas</b></sub></a><br /><a href="#challenge-tomalaforge" title="Create a challenge">🧩</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomalaforge" title="Code">💻</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomalaforge" title="Documentation">📖</a> <a href="#content-tomalaforge" title="Content">🖋</a> <a href="#ideas-tomalaforge" title="Ideas, Planning, & Feedback">🤔</a> <a href="#design-tomalaforge" title="Design">🎨</a> <a href="#translation-fr-tomalaforge" title="Translate in French">🇫🇷</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jdegand"><img src="https://avatars.githubusercontent.com/u/70610011?v=4?s=100" width="100px;" alt="J. Degand"/><br /><sub><b>J. Degand</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=jdegand" title="Documentation">📖</a> <a href="#content-jdegand" title="Content">🖋</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=jdegand" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeveshChau"><img src="https://avatars.githubusercontent.com/u/9509673?v=4?s=100" width="100px;" alt="Devesh Chaudhari"/><br /><sub><b>Devesh Chaudhari</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=DeveshChau" title="Code">💻</a> <a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3ADeveshChau" title="Bug reports">🐛</a> <a href="#challenge-DeveshChau" title="Create a challenge">🧩</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stillst"><img src="https://avatars.githubusercontent.com/u/1463098?v=4?s=100" width="100px;" alt="stillst"/><br /><sub><b>stillst</b></sub></a><br /><a href="#challenge-stillst" title="Create a challenge">🧩</a> <a href="#translation-ru-stillst" title="Translate in Russian">🇷🇺</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alan-bio"><img src="https://avatars.githubusercontent.com/u/31838230?v=4?s=100" width="100px;" alt="Alan Dragicevich"/><br /><sub><b>Alan Dragicevich</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=alan-bio" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/edimitchel"><img src="https://avatars.githubusercontent.com/u/2922851?v=4?s=100" width="100px;" alt="Michel EDIGHOFFER"/><br /><sub><b>Michel EDIGHOFFER</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=edimitchel" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gsgonzalez88"><img src="https://avatars.githubusercontent.com/u/39884678?v=4?s=100" width="100px;" alt="Gerardo Sebastian Gonzalez"/><br /><sub><b>Gerardo Sebastian Gonzalez</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=gsgonzalez88" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marryday"><img src="https://avatars.githubusercontent.com/u/57489315?v=4?s=100" width="100px;" alt="Evseev Yuriy"/><br /><sub><b>Evseev Yuriy</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Amarryday" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomer953"><img src="https://avatars.githubusercontent.com/u/1807493?v=4?s=100" width="100px;" alt="Tomer953"/><br /><sub><b>Tomer953</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Atomer953" title="Bug reports">🐛</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jdegand"><img src="https://avatars.githubusercontent.com/u/70610011?v=4?s=100" width="100px;" alt="J. Degand"/><br /><sub><b>J. Degand</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=jdegand" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeveshChau"><img src="https://avatars.githubusercontent.com/u/9509673?v=4?s=100" width="100px;" alt="Devesh Chaudhari"/><br /><sub><b>Devesh Chaudhari</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=DeveshChau" title="Code">💻</a> <a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3ADeveshChau" title="Bug reports">🐛</a> <a href="#challenge-DeveshChau" title="Create a challenge">🧩</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marryday"><img src="https://avatars.githubusercontent.com/u/57489315?v=4?s=100" width="100px;" alt="Evseev Yuriy"/><br /><sub><b>Evseev Yuriy</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Amarryday" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomer953"><img src="https://avatars.githubusercontent.com/u/1807493?v=4?s=100" width="100px;" alt="Tomer953"/><br /><sub><b>Tomer953</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Atomer953" title="Bug reports">🐛</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmmishchenko"><img src="https://avatars.githubusercontent.com/u/51910160?v=4?s=100" width="100px;" alt="Dmitriy Mishchenko"/><br /><sub><b>Dmitriy Mishchenko</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=dmmishchenko" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.sagardev.com.np"><img src="https://avatars.githubusercontent.com/u/30800393?v=4?s=100" width="100px;" alt="Sagar Devkota"/><br /><sub><b>Sagar Devkota</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=Sagardevkota" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=Sagardevkota" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nelsonguti.dev/"><img src="https://avatars.githubusercontent.com/u/62297014?v=4?s=100" width="100px;" alt="Nelson Gutierrez"/><br /><sub><b>Nelson Gutierrez</b></sub></a><br /><a href="#translation-es-nelsongutidev" title="Translate in Spanish">🇪🇸</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ho-ssain"><img src="https://avatars.githubusercontent.com/u/61125174?v=4?s=100" width="100px;" alt="Hossain K. M."/><br /><sub><b>Hossain K. M.</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=ho-ssain" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kabrunko-dev/"><img src="https://avatars.githubusercontent.com/u/142346548?v=4?s=100" width="100px;" alt="Diogo Nishikawa"/><br /><sub><b>Diogo Nishikawa</b></sub></a><br /><a href="#translation-pt-BR-kabrunko-dev" title="Translate in Brazilian Portuguese">🇧🇷</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kabrunko-dev/"><img src="https://avatars.githubusercontent.com/u/142346548?v=4?s=100" width="100px;" alt="Diogo Nishikawa"/><br /><sub><b>Diogo Nishikawa</b></sub></a><br /><a href="#translation-pt-kabrunko-dev" title="Translate in Portuguese">🇵🇹</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.streamoverlaypro.com"><img src="https://avatars.githubusercontent.com/u/1978642?v=4?s=100" width="100px;" alt="Erick Rodriguez"/><br /><sub><b>Erick Rodriguez</b></sub></a><br /><a href="#translation-es-ErickRodrCodes" title="Translate in Spanish">🇪🇸</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://eduardoroth.dev"><img src="https://avatars.githubusercontent.com/u/5419161?v=4?s=100" width="100px;" alt="Eduardo Roth"/><br /><sub><b>Eduardo Roth</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=eduardoRoth" title="Documentation">📖</a> <a href="#translation-es-eduardoRoth" title="Translate in Spanish">🇪🇸</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default {
|
||||
displayName: 'angular-anchor-scrolling',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/apps/angular/anchor-scrolling',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
|
||||
@@ -5,6 +5,8 @@ import { RouterOutlet } from '@angular/router';
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: ` <router-outlet></router-outlet> `,
|
||||
template: `
|
||||
<router-outlet></router-outlet>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { NavButtonComponent } from './nav-button.component';
|
||||
selector: 'app-foo',
|
||||
template: `
|
||||
Welcome to foo page
|
||||
<nav-button href="home" class="fixed top-3 left-1/2">Home Page</nav-button>
|
||||
<nav-button href="home" class="fixed left-1/2 top-3">Home Page</nav-button>
|
||||
<div class="h-screen bg-blue-200">section 1</div>
|
||||
<div class="h-screen bg-red-200">section 2</div>
|
||||
`,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NavButtonComponent } from './nav-button.component';
|
||||
imports: [NavButtonComponent],
|
||||
selector: 'app-home',
|
||||
template: `
|
||||
<nav-button href="/foo" class="fixed top-3 left-1/2">Foo Page</nav-button>
|
||||
<nav-button href="/foo" class="fixed left-1/2 top-3">Foo Page</nav-button>
|
||||
<div id="top" class="h-screen bg-gray-500">
|
||||
Empty
|
||||
<nav-button href="#bottom">Scroll Bottom</nav-button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { appConfig } from './app/app.config';
|
||||
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)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default {
|
||||
displayName: 'angular-bug-cd',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/apps/angular/bug-cd',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { RouterOutlet } from '@angular/router';
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<h1 class="text-xl px-4 py-2">My Application</h1>
|
||||
<h1 class="px-4 py-2 text-xl">My Application</h1>
|
||||
<section class="flex">
|
||||
<router-outlet name="side" />
|
||||
<div class="border p-4">
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Component } from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-bar',
|
||||
standalone: true,
|
||||
template: ` BarComponent `,
|
||||
template: `
|
||||
BarComponent
|
||||
`,
|
||||
})
|
||||
export class BarComponent {}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Component } from '@angular/core';
|
||||
@Component({
|
||||
selector: 'app-foo',
|
||||
standalone: true,
|
||||
template: `Foo Component `,
|
||||
template: `
|
||||
Foo Component
|
||||
`,
|
||||
})
|
||||
export class FooComponent {}
|
||||
|
||||
@@ -15,7 +15,7 @@ interface MenuItem {
|
||||
template: `
|
||||
<ng-container *ngFor="let menu of menus">
|
||||
<a
|
||||
class="border px-4 py-2 rounded-md"
|
||||
class="rounded-md border px-4 py-2"
|
||||
[routerLink]="menu.path"
|
||||
routerLinkActive="isSelected">
|
||||
{{ menu.name }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
*ngTemplateOutlet="
|
||||
listTemplateRef || emptyRef;
|
||||
context: { $implicit: item, appList: item, index: i }
|
||||
">
|
||||
</ng-container>
|
||||
"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #emptyRef>No Template</ng-template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default {
|
||||
displayName: 'angular-crud',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
globals: {},
|
||||
coverageDirectory: '../../../coverage/apps/angular/crud',
|
||||
transform: {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class AppComponent implements OnInit {
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe((todoUpdated: any) => {
|
||||
this.todos[todoUpdated.id - 1] = todoUpdated;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { enableProdMode, importProvidersFrom } from '@angular/core';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [importProvidersFrom(HttpClientModule)],
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -4,5 +4,5 @@ import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Component } from '@angular/core';
|
||||
standalone: true,
|
||||
imports: [BtnDisabledDirective, BtnHelmetDirective],
|
||||
selector: 'app-root',
|
||||
template: ` <button btnDisabled hlm>Coucou</button> `,
|
||||
template: `
|
||||
<button btnDisabled hlm>Coucou</button>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -16,7 +16,7 @@ interface ProductContext {
|
||||
export class ProductDirective {
|
||||
static ngTemplateContextGuard(
|
||||
dir: ProductDirective,
|
||||
ctx: unknown
|
||||
ctx: unknown,
|
||||
): ctx is ProductContext {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export class CurrencyPipe implements PipeTransform {
|
||||
|
||||
transform(price: number) {
|
||||
return this.currencyService.symbol$.pipe(
|
||||
map((s) => `${String(price)}${s}`)
|
||||
map((s) => `${String(price)}${s}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const currency: Currency[] = [
|
||||
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)
|
||||
map((code) => currency.find((c) => c.code === code)?.symbol ?? code),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default {
|
||||
displayName: 'angular-injection-token',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/apps/angular/injection-token',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
|
||||
@@ -5,15 +5,17 @@ import { RouterLink, RouterOutlet } from '@angular/router';
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, RouterLink],
|
||||
selector: 'app-root',
|
||||
template: ` <div class="flex gap-4 mb-5">
|
||||
<button class="border rounded-md px-4 py-2" routerLink="video">
|
||||
template: `
|
||||
<div class="mb-5 flex gap-4">
|
||||
<button class="rounded-md border px-4 py-2" routerLink="video">
|
||||
Video
|
||||
</button>
|
||||
<button class="border rounded-md px-4 py-2" routerLink="phone">
|
||||
<button class="rounded-md border px-4 py-2" routerLink="phone">
|
||||
Phone
|
||||
</button>
|
||||
</div>
|
||||
<router-outlet />`,
|
||||
<router-outlet />
|
||||
`,
|
||||
host: {
|
||||
class: 'p-10 flex flex-col',
|
||||
},
|
||||
|
||||
@@ -5,10 +5,12 @@ import { TimerContainerComponent } from './timer-container.component';
|
||||
selector: 'app-phone',
|
||||
standalone: true,
|
||||
imports: [TimerContainerComponent],
|
||||
template: `<div class="flex gap-2">
|
||||
template: `
|
||||
<div class="flex gap-2">
|
||||
Phone Call Timer:
|
||||
<p class="italic">(should be 2000s)</p>
|
||||
</div>
|
||||
<timer-container />`,
|
||||
<timer-container />
|
||||
`,
|
||||
})
|
||||
export default class PhoneComponent {}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { DEFAULT_TIMER } from './data';
|
||||
@Component({
|
||||
selector: 'timer',
|
||||
standalone: true,
|
||||
template: ` Timer running {{ timer() }} `,
|
||||
template: `
|
||||
Timer running {{ timer() }}
|
||||
`,
|
||||
})
|
||||
export class TimerComponent {
|
||||
timer = toSignal(interval(DEFAULT_TIMER));
|
||||
|
||||
@@ -5,10 +5,12 @@ import { TimerContainerComponent } from './timer-container.component';
|
||||
selector: 'app-video',
|
||||
standalone: true,
|
||||
imports: [TimerContainerComponent],
|
||||
template: `<div class="flex gap-2">
|
||||
template: `
|
||||
<div class="flex gap-2">
|
||||
Video Call Timer:
|
||||
<p class="italic">(should be the default 1000s)</p>
|
||||
</div>
|
||||
<timer-container />`,
|
||||
<timer-container />
|
||||
`,
|
||||
})
|
||||
export default class VideoComponent {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default {
|
||||
displayName: 'angular-interop-rxjs-signal',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/apps/angular/interop-rxjs-signal',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
|
||||
@@ -5,7 +5,9 @@ import { RouterOutlet } from '@angular/router';
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: `<router-outlet />`,
|
||||
template: `
|
||||
<router-outlet />
|
||||
`,
|
||||
styles: [''],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const appConfig: ApplicationConfig = {
|
||||
redirectTo: '',
|
||||
},
|
||||
],
|
||||
withComponentInputBinding()
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -9,13 +9,25 @@ import { Photo } from '../photo.model';
|
||||
imports: [DatePipe, RouterLink],
|
||||
template: `
|
||||
<img src="{{ photo.url_m }}" alt="{{ photo.title }}" class="image" />
|
||||
<p><span class="font-bold">Title:</span> {{ photo.title }}</p>
|
||||
<p><span class="font-bold">Owner:</span> {{ photo.ownername }}</p>
|
||||
<p><span class="font-bold">Date:</span> {{ photo.datetaken | date }}</p>
|
||||
<p><span class="font-bold">Tags:</span> {{ photo.tags }}</p>
|
||||
<p>
|
||||
<span class="font-bold">Title:</span>
|
||||
{{ photo.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Owner:</span>
|
||||
{{ photo.ownername }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Date:</span>
|
||||
{{ photo.datetaken | date }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Tags:</span>
|
||||
{{ photo.tags }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="border border-black rounded-md px-4 py-2 mt-10"
|
||||
class="mt-10 rounded-md border border-black px-4 py-2"
|
||||
routerLink="">
|
||||
Back
|
||||
</button>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { PhotoStore } from './photos.store';
|
||||
RouterLinkWithHref,
|
||||
],
|
||||
template: `
|
||||
<h2 class="text-xl mb-2">Photos</h2>
|
||||
<h2 class="mb-2 text-xl">Photos</h2>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Search</mat-label>
|
||||
@@ -33,23 +33,23 @@ import { PhotoStore } from './photos.store';
|
||||
type="text"
|
||||
matInput
|
||||
[formControl]="search"
|
||||
placeholder="write an article" />
|
||||
placeholder="find a photo" />
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container *ngrxLet="vm$ as vm">
|
||||
<section class="flex flex-col">
|
||||
<section class="flex gap-3 items-center">
|
||||
<section class="flex items-center gap-3">
|
||||
<button
|
||||
[disabled]="vm.page === 1"
|
||||
[class.bg-gray-400]="vm.page === 1"
|
||||
class="text-xl border rounded-md p-3"
|
||||
class="rounded-md border p-3 text-xl"
|
||||
(click)="store.previousPage()">
|
||||
<
|
||||
</button>
|
||||
<button
|
||||
[disabled]="vm.endOfPage"
|
||||
[class.bg-gray-400]="vm.endOfPage"
|
||||
class="text-xl border rounded-md p-3"
|
||||
class="rounded-md border p-3 text-xl"
|
||||
(click)="store.nextPage()">
|
||||
>
|
||||
</button>
|
||||
@@ -93,7 +93,7 @@ export default class PhotosComponent implements OnInit {
|
||||
this.search.setValue(search);
|
||||
this.formInit = true;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
private formInit = false;
|
||||
@@ -104,8 +104,8 @@ export default class PhotosComponent implements OnInit {
|
||||
this.search.valueChanges.pipe(
|
||||
skipWhile(() => !this.formInit),
|
||||
debounceTime(300),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export class PhotoStore
|
||||
private readonly endOfPage$ = this.select(
|
||||
this.page$,
|
||||
this.pages$,
|
||||
(page, pages) => page === pages
|
||||
(page, pages) => page === pages,
|
||||
);
|
||||
|
||||
readonly vm$ = this.select(
|
||||
@@ -60,7 +60,7 @@ export class PhotoStore
|
||||
loading: this.loading$,
|
||||
error: this.error$,
|
||||
},
|
||||
{ debounce: true }
|
||||
{ debounce: true },
|
||||
);
|
||||
|
||||
ngrxOnStoreInit() {
|
||||
@@ -82,7 +82,7 @@ export class PhotoStore
|
||||
this.select({
|
||||
search: this.search$,
|
||||
page: this.page$,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,21 +91,21 @@ export class PhotoStore
|
||||
...state,
|
||||
search,
|
||||
page: 1,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
readonly nextPage = this.updater(
|
||||
(state): PhotoState => ({
|
||||
...state,
|
||||
page: state.page + 1,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
readonly previousPage = this.updater(
|
||||
(state): PhotoState => ({
|
||||
...state,
|
||||
page: state.page - 1,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
readonly searchPhotos = this.effect<{ search: string; page: number }>(
|
||||
@@ -123,13 +123,13 @@ export class PhotoStore
|
||||
});
|
||||
localStorage.setItem(
|
||||
PHOTO_STATE_KEY,
|
||||
JSON.stringify({ search, page })
|
||||
JSON.stringify({ search, page }),
|
||||
);
|
||||
},
|
||||
(error: unknown) => this.patchState({ error, loading: false })
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(error: unknown) => this.patchState({ error, loading: false }),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
export interface Photo {
|
||||
id: string;
|
||||
title: string;
|
||||
tags: string;
|
||||
owner: string;
|
||||
ownername: string;
|
||||
secret: string;
|
||||
server: string;
|
||||
farm: number;
|
||||
title: string;
|
||||
ispublic: number;
|
||||
isfriend: number;
|
||||
isfamily: number;
|
||||
datetaken: string;
|
||||
datetakengranularity: number;
|
||||
datetakenunknown: string;
|
||||
ownername: string;
|
||||
tags: string;
|
||||
url_q: string;
|
||||
height_q: number;
|
||||
width_q: number;
|
||||
url_m: string;
|
||||
height_m: number;
|
||||
width_m: number;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import { Photo } from './photo.model';
|
||||
|
||||
export interface FlickrAPIResponse {
|
||||
photos: {
|
||||
page: number;
|
||||
pages: number;
|
||||
perpage: number;
|
||||
total: number;
|
||||
photo: Photo[];
|
||||
};
|
||||
stat: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -16,7 +20,7 @@ export class PhotoService {
|
||||
|
||||
public searchPublicPhotos(
|
||||
searchTerm: string,
|
||||
page: number
|
||||
page: number,
|
||||
): Observable<FlickrAPIResponse> {
|
||||
return this.http.get<FlickrAPIResponse>(
|
||||
'https://www.flickr.com/services/rest/',
|
||||
@@ -33,7 +37,7 @@ export class PhotoService {
|
||||
extras: 'tags,date_taken,owner_name,url_q,url_m',
|
||||
api_key: 'c3050d39a5bb308d9921bef0e15c437d',
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
@@ -2,24 +2,26 @@ import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: ` <div class="flex gap-2">
|
||||
template: `
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
routerLink="home"
|
||||
class="border px-4 py-2 border-blue-400 rounded-md">
|
||||
class="rounded-md border border-blue-400 px-4 py-2">
|
||||
Home
|
||||
</button>
|
||||
<button
|
||||
routerLink="admin"
|
||||
class="border px-4 py-2 border-blue-400 rounded-md">
|
||||
class="rounded-md border border-blue-400 px-4 py-2">
|
||||
Admin
|
||||
</button>
|
||||
<button
|
||||
routerLink="user"
|
||||
class="border px-4 py-2 border-blue-400 rounded-md">
|
||||
class="rounded-md border border-blue-400 px-4 py-2">
|
||||
User
|
||||
</button>
|
||||
</div>
|
||||
<router-outlet></router-outlet>`,
|
||||
<router-outlet></router-outlet>
|
||||
`,
|
||||
host: {
|
||||
class: 'flex flex-col p-4 gap-3',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -5,7 +5,9 @@ import { RouterOutlet } from '@angular/router';
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: ` <router-outlet></router-outlet> `,
|
||||
template: `
|
||||
<router-outlet></router-outlet>
|
||||
`,
|
||||
styles: [],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
@@ -4,7 +4,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'button[app-button]',
|
||||
template: `<ng-content></ng-content>`,
|
||||
template: `
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
host: {
|
||||
class: 'border border-blue-700 bg-blue-400 p-2 rounded-sm text-white',
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { UserStore } from './user.store';
|
||||
imports: [InformationComponent, RouterLink, ButtonComponent],
|
||||
selector: 'app-login',
|
||||
template: `
|
||||
<header class="flex gap-3 items-center">
|
||||
<header class="flex items-center gap-3">
|
||||
Log as :
|
||||
<button app-button (click)="admin()">Admin</button>
|
||||
<button app-button (click)="manager()">Manager</button>
|
||||
@@ -26,7 +26,7 @@ import { UserStore } from './user.store';
|
||||
<button app-button (click)="writer()">Writer</button>
|
||||
<button app-button (click)="readerWriter()">Reader and Writer</button>
|
||||
<button app-button (click)="client()">Client</button>
|
||||
<button app-button (click)="everyone()">Client</button>
|
||||
<button app-button (click)="everyone()">Everyone</button>
|
||||
</header>
|
||||
|
||||
<app-information></app-information>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const APP_ROUTES = [
|
||||
path: 'enter',
|
||||
loadComponent: () =>
|
||||
import('./dashboard/admin.component').then(
|
||||
(m) => m.AdminDashboardComponent
|
||||
(m) => m.AdminDashboardComponent,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -4,5 +4,5 @@ import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export default {
|
||||
displayName: 'angular-projection',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
globals: {},
|
||||
coverageDirectory: '../../../coverage/apps/angular/projection',
|
||||
transform: {
|
||||
|
||||
@@ -7,10 +7,12 @@ import { CardComponent } from '../../ui/card/card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-student-card',
|
||||
template: `<app-card
|
||||
template: `
|
||||
<app-card
|
||||
[list]="students"
|
||||
[type]="cardType"
|
||||
customClass="bg-light-green"></app-card>`,
|
||||
customClass="bg-light-green"></app-card>
|
||||
`,
|
||||
standalone: true,
|
||||
styles: [
|
||||
`
|
||||
@@ -25,7 +27,10 @@ export class StudentCardComponent implements OnInit {
|
||||
students: Student[] = [];
|
||||
cardType = CardType.STUDENT;
|
||||
|
||||
constructor(private http: FakeHttpService, private store: StudentStore) {}
|
||||
constructor(
|
||||
private http: FakeHttpService,
|
||||
private store: StudentStore,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
|
||||
|
||||
@@ -7,10 +7,12 @@ import { CardComponent } from '../../ui/card/card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-teacher-card',
|
||||
template: `<app-card
|
||||
template: `
|
||||
<app-card
|
||||
[list]="teachers"
|
||||
[type]="cardType"
|
||||
customClass="bg-light-red"></app-card>`,
|
||||
customClass="bg-light-red"></app-card>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
::ng-deep .bg-light-red {
|
||||
@@ -25,7 +27,10 @@ export class TeacherCardComponent implements OnInit {
|
||||
teachers: Teacher[] = [];
|
||||
cardType = CardType.TEACHER;
|
||||
|
||||
constructor(private http: FakeHttpService, private store: TeacherStore) {}
|
||||
constructor(
|
||||
private http: FakeHttpService,
|
||||
private store: TeacherStore,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CardType } from '../../model/card.model';
|
||||
@Component({
|
||||
selector: 'app-list-item',
|
||||
template: `
|
||||
<div class="border border-grey-300 py-1 px-2 flex justify-between">
|
||||
<div class="border-grey-300 flex justify-between border px-2 py-1">
|
||||
{{ name }}
|
||||
<button (click)="delete(id)">
|
||||
<img class="h-5" src="assets/svg/trash.svg" />
|
||||
@@ -22,7 +22,7 @@ export class ListItemComponent {
|
||||
|
||||
constructor(
|
||||
private teacherStore: TeacherStore,
|
||||
private studentStore: StudentStore
|
||||
private studentStore: StudentStore,
|
||||
) {}
|
||||
|
||||
delete(id: number) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -6,17 +6,19 @@ import { RouterLink, RouterModule } from '@angular/router';
|
||||
standalone: true,
|
||||
imports: [RouterLink, RouterModule, ReactiveFormsModule],
|
||||
selector: 'app-root',
|
||||
template: ` <label for="userName">UserName</label>
|
||||
template: `
|
||||
<label for="userName">UserName</label>
|
||||
<input id="userName" type="text" [formControl]="userName" />
|
||||
<label for="testId">TestId</label>
|
||||
<input id="testId" type="number" [formControl]="testId" />
|
||||
<button
|
||||
[routerLink]="'test/' + testId.value"
|
||||
[routerLink]="'subscription/' + testId.value"
|
||||
[queryParams]="{ user: userName.value }">
|
||||
Test
|
||||
</button>
|
||||
<button routerLink="/">HOME</button>
|
||||
<router-outlet></router-outlet>`,
|
||||
<router-outlet></router-outlet>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {
|
||||
userName = new FormControl();
|
||||
|
||||
@@ -6,7 +6,7 @@ export const appRoutes: Route[] = [
|
||||
loadComponent: () => import('./home.component'),
|
||||
},
|
||||
{
|
||||
path: 'test/:testId',
|
||||
path: 'subscription/:testId',
|
||||
loadComponent: () => import('./test.component'),
|
||||
data: {
|
||||
permission: 'admin',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Component } from '@angular/core';
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
template: `<div>Home</div>`,
|
||||
template: `
|
||||
<div>Home</div>
|
||||
`,
|
||||
})
|
||||
export default class HomeComponent {}
|
||||
|
||||
@@ -2,8 +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',
|
||||
selector: 'app-subscription',
|
||||
standalone: true,
|
||||
imports: [AsyncPipe],
|
||||
template: `
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
console.error(err),
|
||||
);
|
||||
|
||||
36
apps/angular/signal-input/.eslintrc.json
Normal file
36
apps/angular/signal-input/.eslintrc.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
apps/angular/signal-input/README.md
Normal file
13
apps/angular/signal-input/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Signal Input
|
||||
|
||||
> author: thomas-laforge
|
||||
|
||||
### Run Application
|
||||
|
||||
```bash
|
||||
npx nx serve angular-signal-input
|
||||
```
|
||||
|
||||
### Documentation and Instruction
|
||||
|
||||
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/43-signal-input/).
|
||||
73
apps/angular/signal-input/project.json
Normal file
73
apps/angular/signal-input/project.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "angular-signal-input",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/angular/signal-input/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/angular/signal-input",
|
||||
"index": "apps/angular/signal-input/src/index.html",
|
||||
"browser": "apps/angular/signal-input/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/angular/signal-input/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"apps/angular/signal-input/src/favicon.ico",
|
||||
"apps/angular/signal-input/src/assets"
|
||||
],
|
||||
"styles": ["apps/angular/signal-input/src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-signal-input:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-signal-input:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "angular-signal-input:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
45
apps/angular/signal-input/src/app/app.component.ts
Normal file
45
apps/angular/signal-input/src/app/app.component.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { JsonPipe } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { UserComponent } from './user.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [UserComponent, JsonPipe],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex gap-2 ">
|
||||
Name:
|
||||
<input #name class="border" />
|
||||
@if (showUser && !name.value) {
|
||||
<div class="text-sm text-red-500">name required</div>
|
||||
}
|
||||
</div>
|
||||
<div class="flex gap-2 ">
|
||||
LastName:
|
||||
<input #lastName class="border" />
|
||||
</div>
|
||||
<div class="flex gap-2 ">
|
||||
Age:
|
||||
<input type="number" #age class="border" />
|
||||
</div>
|
||||
<button
|
||||
(click)="showUser = true"
|
||||
class="w-fit rounded-md border border-blue-500 bg-blue-200 px-4 py-2">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
@if (showUser && !!name.value) {
|
||||
<app-user
|
||||
[name]="name.value"
|
||||
[lastName]="lastName.value"
|
||||
[age]="age.value" />
|
||||
}
|
||||
`,
|
||||
host: {
|
||||
class: 'p-10 block flex flex-col gap-10',
|
||||
},
|
||||
})
|
||||
export class AppComponent {
|
||||
showUser = false;
|
||||
}
|
||||
5
apps/angular/signal-input/src/app/app.config.ts
Normal file
5
apps/angular/signal-input/src/app/app.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [],
|
||||
};
|
||||
41
apps/angular/signal-input/src/app/user.component.ts
Normal file
41
apps/angular/signal-input/src/app/user.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TitleCasePipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
|
||||
type Category = 'Youth' | 'Junior' | 'Open' | 'Senior';
|
||||
const ageToCategory = (age: number): Category => {
|
||||
if (age < 10) return 'Youth';
|
||||
else if (age < 18) return 'Junior';
|
||||
else if (age < 35) return 'Open';
|
||||
return 'Senior';
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-user',
|
||||
standalone: true,
|
||||
imports: [TitleCasePipe],
|
||||
template: `
|
||||
{{ fullName | titlecase }} plays tennis in the {{ category }} category!!
|
||||
`,
|
||||
host: {
|
||||
class: 'text-xl text-green-800',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UserComponent implements OnChanges {
|
||||
@Input({ required: true }) name!: string;
|
||||
@Input() lastName?: string;
|
||||
@Input() age?: string;
|
||||
|
||||
fullName = '';
|
||||
category: Category = 'Junior';
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.fullName = `${this.name} ${this.lastName ?? ''}`;
|
||||
this.category = ageToCategory(Number(this.age) ?? 0);
|
||||
}
|
||||
}
|
||||
0
apps/angular/signal-input/src/assets/.gitkeep
Normal file
0
apps/angular/signal-input/src/assets/.gitkeep
Normal file
BIN
apps/angular/signal-input/src/favicon.ico
Normal file
BIN
apps/angular/signal-input/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
apps/angular/signal-input/src/index.html
Normal file
13
apps/angular/signal-input/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>angular-signal-input</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
7
apps/angular/signal-input/src/main.ts
Normal file
7
apps/angular/signal-input/src/main.ts
Normal file
@@ -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),
|
||||
);
|
||||
5
apps/angular/signal-input/src/styles.scss
Normal file
5
apps/angular/signal-input/src/styles.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
14
apps/angular/signal-input/tailwind.config.js
Normal file
14
apps/angular/signal-input/tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
|
||||
const { join } = require('path');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
10
apps/angular/signal-input/tsconfig.app.json
Normal file
10
apps/angular/signal-input/tsconfig.app.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||
}
|
||||
7
apps/angular/signal-input/tsconfig.editor.json
Normal file
7
apps/angular/signal-input/tsconfig.editor.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
30
apps/angular/signal-input/tsconfig.json
Normal file
30
apps/angular/signal-input/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.editor.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { TextComponent } from './text.component';
|
||||
<static-text></static-text>
|
||||
<static-text type="error"></static-text>
|
||||
<static-text type="warning"></static-text>
|
||||
<text [font]="15" color="blue">This a a blue text</text>
|
||||
<text [font]="15" color="blue">This is a blue text</text>
|
||||
`,
|
||||
})
|
||||
export class PageComponent {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
36
apps/forms/control-value-accessor/.eslintrc.json
Normal file
36
apps/forms/control-value-accessor/.eslintrc.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
apps/forms/control-value-accessor/README.md
Normal file
13
apps/forms/control-value-accessor/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Control Value Accessor
|
||||
|
||||
> author: stanislav-gavrilov
|
||||
|
||||
### Run Application
|
||||
|
||||
```bash
|
||||
npx nx serve forms-control-value-accessor
|
||||
```
|
||||
|
||||
### Documentation and Instruction
|
||||
|
||||
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/forms/41-control-value-accessor/).
|
||||
22
apps/forms/control-value-accessor/jest.config.ts
Normal file
22
apps/forms/control-value-accessor/jest.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'forms-control-value-accessor',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/apps/forms/control-value-accessor',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/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',
|
||||
],
|
||||
};
|
||||
86
apps/forms/control-value-accessor/project.json
Normal file
86
apps/forms/control-value-accessor/project.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "forms-control-value-accessor",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/forms/control-value-accessor/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/forms/control-value-accessor",
|
||||
"index": "apps/forms/control-value-accessor/src/index.html",
|
||||
"browser": "apps/forms/control-value-accessor/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/forms/control-value-accessor/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"apps/forms/control-value-accessor/src/favicon.ico",
|
||||
"apps/forms/control-value-accessor/src/assets"
|
||||
],
|
||||
"styles": ["apps/forms/control-value-accessor/src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "forms-control-value-accessor:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "forms-control-value-accessor:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "forms-control-value-accessor:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"apps/forms/control-value-accessor/**/*.ts",
|
||||
"apps/forms/control-value-accessor/**/*.html"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/forms/control-value-accessor/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
apps/forms/control-value-accessor/src/app/app.component.ts
Normal file
16
apps/forms/control-value-accessor/src/app/app.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FeedbackFormComponent } from './feedback-form/feedback-form.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [FeedbackFormComponent],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<app-feedback-form (feedBackSubmit)="apiCall($event)"></app-feedback-form>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {
|
||||
apiCall(event: Record<string, string | null>): void {
|
||||
console.log(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<form
|
||||
[formGroup]="feedbackForm"
|
||||
class="feedback-form"
|
||||
(ngSubmit)="submitForm()">
|
||||
<legend class="feedback-form-title">Tell us what you think</legend>
|
||||
<input
|
||||
class="feedback-form-control"
|
||||
[formControl]="feedbackForm.controls.name"
|
||||
placeholder="Name"
|
||||
type="text" />
|
||||
<input
|
||||
class="feedback-form-control"
|
||||
[formControl]="feedbackForm.controls.email"
|
||||
placeholder="Email"
|
||||
type="email" />
|
||||
<app-rating-control (ratingUpdated)="rating = $event"></app-rating-control>
|
||||
<textarea
|
||||
class="feedback-form-control"
|
||||
[formControl]="feedbackForm.controls.comment"
|
||||
placeholder="Сomment text"></textarea>
|
||||
<button
|
||||
class="feedback-form-submit"
|
||||
type="submit"
|
||||
[disabled]="
|
||||
!feedbackForm.touched || rating === null || feedbackForm.invalid
|
||||
">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,50 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.feedback-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
padding: 20px;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.feedback-form-title {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.feedback-form-control {
|
||||
max-height: 200px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 12px 12px 20px;
|
||||
border-radius: 0;
|
||||
background-color: #fbfbfb;
|
||||
color: #3c3c3c;
|
||||
font-size: 18px;
|
||||
|
||||
&:focus {
|
||||
padding: 10px 10px 10px 18px;
|
||||
border: 2px solid #054ada;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-form-submit {
|
||||
padding: 10px;
|
||||
background-color: #054ada;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
|
||||
&[disabled] {
|
||||
background-color: #cccccc;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { RatingControlComponent } from '../rating-control/rating-control.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [RatingControlComponent, ReactiveFormsModule],
|
||||
selector: 'app-feedback-form',
|
||||
templateUrl: 'feedback-form.component.html',
|
||||
styleUrls: ['feedback-form.component.scss'],
|
||||
})
|
||||
export class FeedbackFormComponent {
|
||||
@Output()
|
||||
readonly feedBackSubmit: EventEmitter<Record<string, string | null>> =
|
||||
new EventEmitter<Record<string, string | null>>();
|
||||
|
||||
readonly feedbackForm = new FormGroup({
|
||||
name: new FormControl('', {
|
||||
validators: Validators.required,
|
||||
}),
|
||||
email: new FormControl('', {
|
||||
validators: Validators.required,
|
||||
}),
|
||||
comment: new FormControl(),
|
||||
});
|
||||
|
||||
rating: string | null = null;
|
||||
|
||||
submitForm(): void {
|
||||
this.feedBackSubmit.emit({
|
||||
...this.feedbackForm.value,
|
||||
rating: this.rating,
|
||||
});
|
||||
|
||||
this.feedbackForm.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="star" width="50" height="50" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M31.77 11.857H19.74L15.99.5l-3.782 11.357H0l9.885 6.903-3.692 11.21 9.736-7.05 9.796 6.962-3.722-11.18 9.766-6.845z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
<div class="rating">
|
||||
@for (item of [].constructor(5); track item) {
|
||||
<svg
|
||||
class="star"
|
||||
[class.star-active]="isStarActive($index, value)"
|
||||
(click)="setRating($index)">
|
||||
<use xlink:href="#star"></use>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
|
After Width: | Height: | Size: 547 B |
@@ -0,0 +1,26 @@
|
||||
.rating {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
|
||||
&:hover {
|
||||
.star {
|
||||
fill: #ffd055;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.star {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
fill: #cccccc;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover ~ .star {
|
||||
fill: #d8d8d8;
|
||||
}
|
||||
}
|
||||
|
||||
.star-active {
|
||||
fill: #ffd055 !important;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-rating-control',
|
||||
templateUrl: 'rating-control.component.html',
|
||||
styleUrls: ['rating-control.component.scss'],
|
||||
})
|
||||
export class RatingControlComponent {
|
||||
@Output()
|
||||
readonly ratingUpdated: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
value: number | null = null;
|
||||
|
||||
setRating(index: number): void {
|
||||
this.value = index + 1;
|
||||
this.ratingUpdated.emit(`${this.value}`);
|
||||
}
|
||||
|
||||
isStarActive(index: number, value: number | null): boolean {
|
||||
return value ? index < value : false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user