Compare commits
75 Commits
da6e9d38a9
...
01376c3611
| Author | SHA1 | Date | |
|---|---|---|---|
| 01376c3611 | |||
| 24a9ad7b5f | |||
| 0f0fa78caa | |||
| 2384cfd307 | |||
| 419ca871a8 | |||
| 882b0e2132 | |||
| 06f1769f5c | |||
| fd299219ce | |||
| 7ee1c4c661 | |||
| 080f97e597 | |||
| 71c67e23fd | |||
| 361fd6ce57 | |||
| ed3d749ba4 | |||
| 7b4c95678e | |||
| 3926f4d254 | |||
| 3b5205e124 | |||
| e1b1f6d743 | |||
| 5e4adedd27 | |||
| d0362e2d7d | |||
| dc3def94c4 | |||
| 7c545e2b1e | |||
| 70ff68930d | |||
| 08e07ab3d0 | |||
| 0c0f4024df | |||
| be86d2353d | |||
| 70b7627076 | |||
| 3b6b660284 | |||
| a18b3d87d9 | |||
| d321d2e70f | |||
| 458cf97ba0 | |||
| 9efb64eea8 | |||
| cb13d9d81a | |||
| e49c8bd89c | |||
| fdbd500005 | |||
| 01ac16fbdc | |||
| a41c4a7761 | |||
| d25142c648 | |||
| 7e349909b9 | |||
| de99689fb0 | |||
| 58558b3fca | |||
| f53250dd3b | |||
| 5f1c3062ab | |||
| ccde994797 | |||
| 74ba14c3de | |||
| f74d15713c | |||
| 87d7557cf3 | |||
| a12f66e594 | |||
| 57f721f85a | |||
| 6732e0b974 | |||
| 5f37892ecb | |||
| e5c7146434 | |||
| efe3ed1ab7 | |||
| 106be224e7 | |||
| dfdeeb4d57 | |||
| c501d9aada | |||
| ed4ed94e9b | |||
| e105ce1942 | |||
| 44005a0aa5 | |||
| a8fd369718 | |||
| 54ac7e0210 | |||
| 158440d5c8 | |||
| 65fb14aea5 | |||
| bda4cb9136 | |||
| 2f1de6bea3 | |||
| 515b762862 | |||
| bd48ea25b7 | |||
| 8eadb495d5 | |||
| ec61aabbf8 | |||
| 3df4b2d842 | |||
| be0b730fb0 | |||
| 7f9801defd | |||
| 5ac2e62daf | |||
| cc85fec421 | |||
| 1132199c98 | |||
| a2316fcace |
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
11
angular.json
@@ -47,12 +47,19 @@
|
|||||||
"maximumError": "8kB"
|
"maximumError": "8kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all",
|
||||||
|
"serviceWorker": "ngsw-config.json"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
|
|||||||
30
ngsw-config.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||||
|
"index": "/index.html",
|
||||||
|
"assetGroups": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"installMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/favicon.ico",
|
||||||
|
"/index.csr.html",
|
||||||
|
"/index.html",
|
||||||
|
"/manifest.webmanifest",
|
||||||
|
"/*.css",
|
||||||
|
"/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"installMode": "lazy",
|
||||||
|
"updateMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
451
package-lock.json
generated
@@ -8,20 +8,21 @@
|
|||||||
"name": "agenda-web",
|
"name": "agenda-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^20.3.0",
|
"@angular/common": "^20.3.15",
|
||||||
"@angular/compiler": "^20.3.0",
|
"@angular/compiler": "^20.3.15",
|
||||||
"@angular/core": "^20.3.0",
|
"@angular/core": "^20.3.15",
|
||||||
"@angular/forms": "^20.3.0",
|
"@angular/forms": "^20.3.15",
|
||||||
"@angular/platform-browser": "^20.3.0",
|
"@angular/platform-browser": "^20.3.15",
|
||||||
"@angular/router": "^20.3.0",
|
"@angular/router": "^20.3.15",
|
||||||
|
"@angular/service-worker": "^20.3.15",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.3.9",
|
"@angular/build": "^20.3.13",
|
||||||
"@angular/cli": "^20.3.9",
|
"@angular/cli": "^20.3.13",
|
||||||
"@angular/compiler-cli": "^20.3.0",
|
"@angular/compiler-cli": "^20.3.15",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.9.0",
|
"jasmine-core": "~5.9.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
@@ -256,13 +257,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-devkit/architect": {
|
"node_modules/@angular-devkit/architect": {
|
||||||
"version": "0.2003.9",
|
"version": "0.2003.13",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.9.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.13.tgz",
|
||||||
"integrity": "sha512-p0GO2H8hiZjRHI9sm4tXTF3OpWaEnkqvB0GBGJfGp8RvpPfDA2t3j2NAUNtd75H+B0xdfyWLmNq9YJGpy6gznA==",
|
"integrity": "sha512-JyH6Af6PNC1IHJToColFk1RaXDU87mpPjz7M5sWDfn8bC+KBipw6dSdRkCEuw0D9HY1lZkC9EBV9k9GhpvHjCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "20.3.9",
|
"@angular-devkit/core": "20.3.13",
|
||||||
"rxjs": "7.8.2"
|
"rxjs": "7.8.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -272,9 +273,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-devkit/core": {
|
"node_modules/@angular-devkit/core": {
|
||||||
"version": "20.3.9",
|
"version": "20.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.13.tgz",
|
||||||
"integrity": "sha512-bXsAGIUb4p60x548YmvnMvjwd3FwWz6re1uTM7dV0XH8nQn3XMhOQ3Q3sAckzJHxkDuaRhB3K/a4kupoOmVfTQ==",
|
"integrity": "sha512-/D84T1Caxll3I2sRihPDR9UaWBhF50M+tAX15PdP6uSh/TxwAlLl9p7Rm1bD0mPjPercqaEKA+h9a9qLP16hug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -300,13 +301,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-devkit/schematics": {
|
"node_modules/@angular-devkit/schematics": {
|
||||||
"version": "20.3.9",
|
"version": "20.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.13.tgz",
|
||||||
"integrity": "sha512-oaIjAKPmHMZBTC0met5M7dbXBeZnCNwmHacT/kBHNVBAz/NI95fuAfb2P0Jxt7gWdQXejDSxWp0tL+sZIyO0xw==",
|
"integrity": "sha512-hdMKY4rUTko8xqeWYGnwwDYDomkeOoLsYsP6SdaHWK7hpGvzWsT6Q/aIv8J8NrCYkLu+M+5nLiKOooweUZu3GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "20.3.9",
|
"@angular-devkit/core": "20.3.13",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"magic-string": "0.30.17",
|
"magic-string": "0.30.17",
|
||||||
"ora": "8.2.0",
|
"ora": "8.2.0",
|
||||||
@@ -319,14 +320,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/build": {
|
"node_modules/@angular/build": {
|
||||||
"version": "20.3.9",
|
"version": "20.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.13.tgz",
|
||||||
"integrity": "sha512-Ulimvg6twPSCraaZECEmENfKBlD4M1yqeHlg6dCzFNM4xcwaGUnuG6O3cIQD59DaEvaG73ceM2y8ftYdxAwFow==",
|
"integrity": "sha512-/5pM3ZS+lLkZgA+n6TMmNV8I6t9Ow1C6Vkj6bXqWeOgFDH5LwnIEZFAKzEDBkCGos0m2gPKPcREcDD5tfp9h4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "2.3.0",
|
"@ampproject/remapping": "2.3.0",
|
||||||
"@angular-devkit/architect": "0.2003.9",
|
"@angular-devkit/architect": "0.2003.13",
|
||||||
"@babel/core": "7.28.3",
|
"@babel/core": "7.28.3",
|
||||||
"@babel/helper-annotate-as-pure": "7.27.3",
|
"@babel/helper-annotate-as-pure": "7.27.3",
|
||||||
"@babel/helper-split-export-declaration": "7.24.7",
|
"@babel/helper-split-export-declaration": "7.24.7",
|
||||||
@@ -368,7 +369,7 @@
|
|||||||
"@angular/platform-browser": "^20.0.0",
|
"@angular/platform-browser": "^20.0.0",
|
||||||
"@angular/platform-server": "^20.0.0",
|
"@angular/platform-server": "^20.0.0",
|
||||||
"@angular/service-worker": "^20.0.0",
|
"@angular/service-worker": "^20.0.0",
|
||||||
"@angular/ssr": "^20.3.9",
|
"@angular/ssr": "^20.3.13",
|
||||||
"karma": "^6.4.0",
|
"karma": "^6.4.0",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"ng-packagr": "^20.0.0",
|
"ng-packagr": "^20.0.0",
|
||||||
@@ -418,19 +419,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/cli": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "20.3.9",
|
"version": "20.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.13.tgz",
|
||||||
"integrity": "sha512-4eKpRDg96B20yrKJqjA24zgxYy1RiRd70FvF/KG1hqSowsWwtzydtEJ3VM6iFWS9t1D8truuVpKjMEnn1Y274A==",
|
"integrity": "sha512-G78I/HDJULloS2LSqfUfbmBlhDCbcWujIRWfuMnGsRf82TyGA2OEPe3IA/F8MrJfeOzPQim2fMyn24MqHL40Vg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/architect": "0.2003.9",
|
"@angular-devkit/architect": "0.2003.13",
|
||||||
"@angular-devkit/core": "20.3.9",
|
"@angular-devkit/core": "20.3.13",
|
||||||
"@angular-devkit/schematics": "20.3.9",
|
"@angular-devkit/schematics": "20.3.13",
|
||||||
"@inquirer/prompts": "7.8.2",
|
"@inquirer/prompts": "7.8.2",
|
||||||
"@listr2/prompt-adapter-inquirer": "3.0.1",
|
"@listr2/prompt-adapter-inquirer": "3.0.1",
|
||||||
"@modelcontextprotocol/sdk": "1.17.3",
|
"@modelcontextprotocol/sdk": "1.24.0",
|
||||||
"@schematics/angular": "20.3.9",
|
"@schematics/angular": "20.3.13",
|
||||||
"@yarnpkg/lockfile": "1.1.0",
|
"@yarnpkg/lockfile": "1.1.0",
|
||||||
"algoliasearch": "5.35.0",
|
"algoliasearch": "5.35.0",
|
||||||
"ini": "5.0.0",
|
"ini": "5.0.0",
|
||||||
@@ -441,7 +442,7 @@
|
|||||||
"resolve": "1.22.10",
|
"resolve": "1.22.10",
|
||||||
"semver": "7.7.2",
|
"semver": "7.7.2",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "3.25.76"
|
"zod": "4.1.13"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"ng": "bin/ng.js"
|
"ng": "bin/ng.js"
|
||||||
@@ -453,9 +454,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/common": {
|
"node_modules/@angular/common": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.15.tgz",
|
||||||
"integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==",
|
"integrity": "sha512-k4mCXWRFiOHK3bUKfWkRQQ8KBPxW8TAJuKLYCsSHPCpMz6u0eA1F0VlrnOkZVKWPI792fOaEAWH2Y4PTaXlUHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@@ -464,14 +465,14 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/core": "20.3.10",
|
"@angular/core": "20.3.15",
|
||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/compiler": {
|
"node_modules/@angular/compiler": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.15.tgz",
|
||||||
"integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==",
|
"integrity": "sha512-lMicIAFAKZXa+BCZWs3soTjNQPZZXrF/WMVDinm8dQcggNarnDj4UmXgKSyXkkyqK5SLfnLsXVzrX6ndVT6z7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@@ -481,9 +482,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/compiler-cli": {
|
"node_modules/@angular/compiler-cli": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.15.tgz",
|
||||||
"integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==",
|
"integrity": "sha512-8sJoxodxsfyZ8eJ5r6Bx7BCbazXYgsZ1+dE8t5u5rTQ6jNggwNtYEzkyReoD5xvP+MMtRkos3xpwq4rtFnpI6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -504,7 +505,7 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/compiler": "20.3.10",
|
"@angular/compiler": "20.3.15",
|
||||||
"typescript": ">=5.8 <6.0"
|
"typescript": ">=5.8 <6.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -514,9 +515,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/core": {
|
"node_modules/@angular/core": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.15.tgz",
|
||||||
"integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==",
|
"integrity": "sha512-NMbX71SlTZIY9+rh/SPhRYFJU0pMJYW7z/TBD4lqiO+b0DTOIg1k7Pg9ydJGqSjFO1Z4dQaA6TteNuF99TJCNw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@@ -525,7 +526,7 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/compiler": "20.3.10",
|
"@angular/compiler": "20.3.15",
|
||||||
"rxjs": "^6.5.3 || ^7.4.0",
|
"rxjs": "^6.5.3 || ^7.4.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@@ -539,9 +540,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/forms": {
|
"node_modules/@angular/forms": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.15.tgz",
|
||||||
"integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==",
|
"integrity": "sha512-gS5hQkinq52pm/7mxz4yHPCzEcmRWjtUkOVddPH0V1BW/HMni/p4Y6k2KqKBeGb9p8S5EAp6PDxDVLOPukp3mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@@ -550,16 +551,16 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "20.3.10",
|
"@angular/common": "20.3.15",
|
||||||
"@angular/core": "20.3.10",
|
"@angular/core": "20.3.15",
|
||||||
"@angular/platform-browser": "20.3.10",
|
"@angular/platform-browser": "20.3.15",
|
||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/platform-browser": {
|
"node_modules/@angular/platform-browser": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.15.tgz",
|
||||||
"integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==",
|
"integrity": "sha512-TxRM/wTW/oGXv/3/Iohn58yWoiYXOaeEnxSasiGNS1qhbkcKtR70xzxW6NjChBUYAixz2ERkLURkpx3pI8Q6Dw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@@ -568,9 +569,9 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/animations": "20.3.10",
|
"@angular/animations": "20.3.15",
|
||||||
"@angular/common": "20.3.10",
|
"@angular/common": "20.3.15",
|
||||||
"@angular/core": "20.3.10"
|
"@angular/core": "20.3.15"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@angular/animations": {
|
"@angular/animations": {
|
||||||
@@ -579,9 +580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/router": {
|
"node_modules/@angular/router": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.15.tgz",
|
||||||
"integrity": "sha512-Z03cfH1jgQ7XMDJj4R8qAGqivcvhdG3wYBwaiN1K1ODBgPhbFKNeD4stKqYp7xBNtswmM2O2jMxrL/Djwju4Gg==",
|
"integrity": "sha512-6+qgk8swGSoAu7ISSY//GatAyCP36hEvvUgvjbZgkXLLH9yUQxdo77ij05aJ5s0OyB25q/JkqS8VTY0z1yE9NQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
@@ -590,9 +591,28 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "20.3.10",
|
"@angular/common": "20.3.15",
|
||||||
"@angular/core": "20.3.10",
|
"@angular/core": "20.3.15",
|
||||||
"@angular/platform-browser": "20.3.10",
|
"@angular/platform-browser": "20.3.15",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@angular/service-worker": {
|
||||||
|
"version": "20.3.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-20.3.15.tgz",
|
||||||
|
"integrity": "sha512-HCptODPVWg30XJwSueOz2zqsJjQ1chSscTs7FyIQcfuCTTthO35Lvz2Gtct8/GNHel9QNvvVwA5jrLjsU4dt1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ngsw-config": "ngsw-config.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": "20.3.15",
|
||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1953,13 +1973,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.17.3",
|
"version": "1.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.0.tgz",
|
||||||
"integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==",
|
"integrity": "sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.6",
|
"ajv": "^8.17.1",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.5",
|
"cross-spawn": "^7.0.5",
|
||||||
@@ -1967,39 +1988,28 @@
|
|||||||
"eventsource-parser": "^3.0.0",
|
"eventsource-parser": "^3.0.0",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"jose": "^6.1.1",
|
||||||
"pkce-challenge": "^5.0.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.0",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.25 || ^4.0",
|
||||||
"zod-to-json-schema": "^3.24.1"
|
"zod-to-json-schema": "^3.25.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@cfworker/json-schema": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": false
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
|
|
||||||
"version": "6.12.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.1",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"json-schema-traverse": "^0.4.1",
|
|
||||||
"uri-js": "^4.2.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||||
@@ -3355,14 +3365,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@schematics/angular": {
|
"node_modules/@schematics/angular": {
|
||||||
"version": "20.3.9",
|
"version": "20.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.13.tgz",
|
||||||
"integrity": "sha512-XkgTwGhhrx+MVi2+TFO32d6Es5Uezzx7Y7B/e2ulDlj08bizxQj+9wkeLt5+bR8JWODHpEntZn/Xd5WvXnODGA==",
|
"integrity": "sha512-ETJ1budKmrkdxojo5QP6TPr6zQZYGxtWWf8NrX1cBIS851zPCmFkKyhSFLZsoksariYF/LP8ljvm8tlcIzt/XA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "20.3.9",
|
"@angular-devkit/core": "20.3.13",
|
||||||
"@angular-devkit/schematics": "20.3.9",
|
"@angular-devkit/schematics": "20.3.13",
|
||||||
"jsonc-parser": "3.3.1"
|
"jsonc-parser": "3.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3787,37 +3797,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
||||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.3",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.7.0",
|
||||||
"on-finished": "^2.4.1",
|
"on-finished": "^2.4.1",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.1",
|
||||||
"type-is": "^2.0.0"
|
"type-is": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/body-parser/node_modules/iconv-lite": {
|
"funding": {
|
||||||
"version": "0.6.3",
|
"type": "opencollective",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"url": "https://opencollective.com/express"
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/boolbase": {
|
"node_modules/boolbase": {
|
||||||
@@ -4324,16 +4325,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/content-type": {
|
"node_modules/content-type": {
|
||||||
@@ -4951,19 +4953,20 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "5.1.0",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.1",
|
||||||
"content-disposition": "^1.0.0",
|
"content-disposition": "^1.0.0",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"cookie": "^0.7.1",
|
"cookie": "^0.7.1",
|
||||||
"cookie-signature": "^1.2.1",
|
"cookie-signature": "^1.2.1",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
"encodeurl": "^2.0.0",
|
"encodeurl": "^2.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"etag": "^1.8.1",
|
"etag": "^1.8.1",
|
||||||
@@ -5023,13 +5026,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-json-stable-stringify": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
@@ -5079,9 +5075,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5093,7 +5089,11 @@
|
|||||||
"statuses": "^2.0.1"
|
"statuses": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
@@ -5910,6 +5910,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -6795,16 +6805,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-types": {
|
"node_modules/mime-types": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "^1.54.0"
|
"mime-db": "^1.54.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-function": {
|
"node_modules/mimic-function": {
|
||||||
@@ -7796,9 +7810,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkce-challenge": {
|
"node_modules/pkce-challenge": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||||
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
|
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -7923,21 +7937,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "~3.1.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "~2.0.1",
|
||||||
"iconv-lite": "0.7.0",
|
"iconv-lite": "~0.7.0",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "~1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raw-body/node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -8126,27 +8161,6 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/safe-regex-test": {
|
"node_modules/safe-regex-test": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
@@ -8207,32 +8221,57 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.5",
|
"debug": "^4.4.3",
|
||||||
"encodeurl": "^2.0.0",
|
"encodeurl": "^2.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"etag": "^1.8.1",
|
"etag": "^1.8.1",
|
||||||
"fresh": "^2.0.0",
|
"fresh": "^2.0.0",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.1",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.2",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"on-finished": "^2.4.1",
|
"on-finished": "^2.4.1",
|
||||||
"range-parser": "^1.2.1",
|
"range-parser": "^1.2.1",
|
||||||
"statuses": "^2.0.1"
|
"statuses": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||||
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -8243,6 +8282,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
@@ -9165,26 +9208,6 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uri-js": {
|
|
||||||
"version": "4.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/uri-js/node_modules/punycode": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -9630,9 +9653,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -9640,13 +9663,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-to-json-schema": {
|
"node_modules/zod-to-json-schema": {
|
||||||
"version": "3.24.6",
|
"version": "3.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.25 || ^4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zone.js": {
|
"node_modules/zone.js": {
|
||||||
|
|||||||
19
package.json
@@ -22,20 +22,21 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^20.3.0",
|
"@angular/common": "^20.3.15",
|
||||||
"@angular/compiler": "^20.3.0",
|
"@angular/compiler": "^20.3.15",
|
||||||
"@angular/core": "^20.3.0",
|
"@angular/core": "^20.3.15",
|
||||||
"@angular/forms": "^20.3.0",
|
"@angular/forms": "^20.3.15",
|
||||||
"@angular/platform-browser": "^20.3.0",
|
"@angular/platform-browser": "^20.3.15",
|
||||||
"@angular/router": "^20.3.0",
|
"@angular/router": "^20.3.15",
|
||||||
|
"@angular/service-worker": "^20.3.15",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.3.9",
|
"@angular/build": "^20.3.13",
|
||||||
"@angular/cli": "^20.3.9",
|
"@angular/cli": "^20.3.13",
|
||||||
"@angular/compiler-cli": "^20.3.0",
|
"@angular/compiler-cli": "^20.3.15",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.9.0",
|
"jasmine-core": "~5.9.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 578 B |
BIN
public/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/sp_flag.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/images/uk_flag.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
58
public/manifest.webmanifest
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "Gabigenda",
|
||||||
|
"short_name": "Gabigenda",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "./",
|
||||||
|
"background_color": "#ff0000",
|
||||||
|
"start_url": "./",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,12 +1,33 @@
|
|||||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
import {
|
||||||
|
ApplicationConfig,
|
||||||
|
InjectionToken,
|
||||||
|
provideBrowserGlobalErrorListeners,
|
||||||
|
provideZoneChangeDetection, isDevMode,
|
||||||
|
} from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
import { strings } from './strings';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { Language } from './types/Language.type';
|
||||||
|
import { provideServiceWorker } from '@angular/service-worker';
|
||||||
|
|
||||||
|
const STRINGS_TOKEN = 'strings';
|
||||||
|
export const STRINGS_INJECTOR = new InjectionToken<typeof strings>(STRINGS_TOKEN);
|
||||||
|
|
||||||
|
const LANGUAGE = 'LS_LANGUAGE';
|
||||||
|
export const LS_LANGUAGE = new InjectionToken<Language>(LANGUAGE);
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes)
|
provideRouter(routes),
|
||||||
]
|
provideHttpClient(),
|
||||||
|
{ provide: STRINGS_INJECTOR, useValue: strings },
|
||||||
|
{ provide: LS_LANGUAGE, useValue: 'language' }, provideServiceWorker('ngsw-worker.js', {
|
||||||
|
enabled: !isDevMode(),
|
||||||
|
registrationStrategy: 'registerWhenStable:30000'
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
344
src/app/app.html
@@ -1,342 +1,4 @@
|
|||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
@if(this.showNotification()) {
|
||||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
<app-notification></app-notification>
|
||||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
--bright-blue: oklch(51.01% 0.274 263.83);
|
|
||||||
--electric-violet: oklch(53.18% 0.28 296.97);
|
|
||||||
--french-violet: oklch(47.66% 0.246 305.88);
|
|
||||||
--vivid-pink: oklch(69.02% 0.277 332.77);
|
|
||||||
--hot-red: oklch(61.42% 0.238 15.34);
|
|
||||||
--orange-red: oklch(63.32% 0.24 31.68);
|
|
||||||
|
|
||||||
--gray-900: oklch(19.37% 0.006 300.98);
|
|
||||||
--gray-700: oklch(36.98% 0.014 302.71);
|
|
||||||
--gray-400: oklch(70.9% 0.015 304.04);
|
|
||||||
|
|
||||||
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
var(--orange-red) 0%,
|
|
||||||
var(--vivid-pink) 50%,
|
|
||||||
var(--electric-violet) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--orange-red) 0%,
|
|
||||||
var(--vivid-pink) 50%,
|
|
||||||
var(--electric-violet) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
|
|
||||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol";
|
|
||||||
box-sizing: border-box;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
<router-outlet></router-outlet>
|
||||||
h1 {
|
|
||||||
font-size: 3.125rem;
|
|
||||||
color: var(--gray-900);
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 100%;
|
|
||||||
letter-spacing: -0.125rem;
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol";
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--gray-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: inherit;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.angular-logo {
|
|
||||||
max-width: 9.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h1 {
|
|
||||||
margin-top: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content p {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
width: 1px;
|
|
||||||
background: var(--red-to-pink-to-purple-vertical-gradient);
|
|
||||||
margin-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
|
||||||
color: var(--pill-accent);
|
|
||||||
padding-inline: 0.75rem;
|
|
||||||
padding-block: 0.375rem;
|
|
||||||
border-radius: 2.75rem;
|
|
||||||
border: 0;
|
|
||||||
transition: background 0.3s ease;
|
|
||||||
font-family: var(--inter-font);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
letter-spacing: -0.00875rem;
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill:hover {
|
|
||||||
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group .pill:nth-child(6n + 1) {
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
}
|
|
||||||
.pill-group .pill:nth-child(6n + 2) {
|
|
||||||
--pill-accent: var(--electric-violet);
|
|
||||||
}
|
|
||||||
.pill-group .pill:nth-child(6n + 3) {
|
|
||||||
--pill-accent: var(--french-violet);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group .pill:nth-child(6n + 4),
|
|
||||||
.pill-group .pill:nth-child(6n + 5),
|
|
||||||
.pill-group .pill:nth-child(6n + 6) {
|
|
||||||
--pill-accent: var(--hot-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group svg {
|
|
||||||
margin-inline-start: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.73rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links path {
|
|
||||||
transition: fill 0.3s ease;
|
|
||||||
fill: var(--gray-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links a:hover svg path {
|
|
||||||
fill: var(--gray-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
.content {
|
|
||||||
flex-direction: column;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
|
||||||
margin-block: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<main class="main">
|
|
||||||
<div class="content">
|
|
||||||
<div class="left-side">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 982 239"
|
|
||||||
fill="none"
|
|
||||||
class="angular-logo"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#a)">
|
|
||||||
<path
|
|
||||||
fill="url(#b)"
|
|
||||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="url(#c)"
|
|
||||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<radialGradient
|
|
||||||
id="c"
|
|
||||||
cx="0"
|
|
||||||
cy="0"
|
|
||||||
r="1"
|
|
||||||
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="#FF41F8" />
|
|
||||||
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
|
|
||||||
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="b"
|
|
||||||
x1="0"
|
|
||||||
x2="982"
|
|
||||||
y1="192"
|
|
||||||
y2="192"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="#F0060B" />
|
|
||||||
<stop offset="0" stop-color="#F0070C" />
|
|
||||||
<stop offset=".526" stop-color="#CC26D5" />
|
|
||||||
<stop offset="1" stop-color="#7702FF" />
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
<h1>Hello, {{ title() }}</h1>
|
|
||||||
<p>Congratulations! Your app is running. 🎉</p>
|
|
||||||
</div>
|
|
||||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
|
||||||
<div class="right-side">
|
|
||||||
<div class="pill-group">
|
|
||||||
@for (item of [
|
|
||||||
{ title: 'Explore the Docs', link: 'https://angular.dev' },
|
|
||||||
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
|
|
||||||
{ title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
|
|
||||||
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
|
|
||||||
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
|
|
||||||
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
|
|
||||||
]; track item.title) {
|
|
||||||
<a
|
|
||||||
class="pill"
|
|
||||||
[href]="item.link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
width="14"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a
|
|
||||||
href="https://github.com/angular/angular"
|
|
||||||
aria-label="Github"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="25"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 25 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Github"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://twitter.com/angular"
|
|
||||||
aria-label="Twitter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Twitter"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
|
||||||
aria-label="Youtube"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="29"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 29 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Youtube"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
|
|
||||||
|
|
||||||
<router-outlet />
|
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
import { Main } from './pages/main/main';
|
||||||
|
import { Edit } from './pages/edit/edit';
|
||||||
|
import { contactResolver } from './pages/edit/contact-resolver';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: Main,
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
component: Edit,
|
||||||
|
resolve: { contact: contactResolver },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
|
import { strings } from './strings';
|
||||||
|
import { STRINGS_INJECTOR } from './app.config';
|
||||||
|
import { Notifier } from './services/notifier';
|
||||||
|
import { signal } from '@angular/core';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
|
let notifier: jasmine.SpyObj<Notifier>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
notifier = jasmine.createSpyObj(Notifier.name, [], {
|
||||||
|
notification$: signal<Notification | null>(null),
|
||||||
|
});
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [App],
|
imports: [App],
|
||||||
|
providers: [
|
||||||
|
{ provide: STRINGS_INJECTOR, useValue: strings },
|
||||||
|
{ provide: Notifier, useValue: notifier },
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create the app', () => {
|
it('should create the app', () => {
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(App);
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
|
TestBed.tick();
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render title', () => {
|
|
||||||
const fixture = TestBed.createComponent(App);
|
|
||||||
fixture.detectChanges();
|
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
|
||||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, agenda-web');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Component, signal } from '@angular/core';
|
import { Component, inject, computed } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { Notification } from './components/notification/notification';
|
||||||
|
import { Notifier } from './services/notifier';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet, Notification],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss',
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
protected readonly title = signal('agenda-web');
|
private readonly notifier = inject(Notifier);
|
||||||
|
showNotification = computed(() => this.notifier.notification$() !== null);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/app/components/card/card.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div [style.backgroundColor]="bgColor()" class="card shadow">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
15
src/app/components/card/card.scss
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
-webkit-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
-moz-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
23
src/app/components/card/card.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Card } from './card';
|
||||||
|
|
||||||
|
describe('Card', () => {
|
||||||
|
let component: Card;
|
||||||
|
let fixture: ComponentFixture<Card>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Card]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Card);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/app/components/card/card.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './card.html',
|
||||||
|
styleUrl: './card.scss',
|
||||||
|
})
|
||||||
|
export class Card {
|
||||||
|
bgColor = input('white');
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div>
|
||||||
|
<a tabindex="0" class="btn btn--edit" (click)="editContact.emit()">
|
||||||
|
<i class="fas fa-pen-square"></i></a>
|
||||||
|
<button class="btn btn--delete" type="button" (click)="deleteContact.emit()">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--edit {
|
||||||
|
margin-right: 4.44995px;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--delete {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ContactActionsBar } from './contact-actions-bar';
|
||||||
|
|
||||||
|
describe('ContactActionsBar', () => {
|
||||||
|
let component: ContactActionsBar;
|
||||||
|
let fixture: ComponentFixture<ContactActionsBar>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContactActionsBar]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ContactActionsBar);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, output } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-contact-actions-bar',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './contact-actions-bar.html',
|
||||||
|
styleUrl: './contact-actions-bar.scss',
|
||||||
|
})
|
||||||
|
export class ContactActionsBar {
|
||||||
|
deleteContact = output();
|
||||||
|
editContact = output();
|
||||||
|
}
|
||||||
30
src/app/components/contact-form/contact-form.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<form [formGroup]="form()" (ngSubmit)="handleSubmit(form().value)">
|
||||||
|
<ng-content select="[slot='header']"></ng-content>
|
||||||
|
|
||||||
|
<div class="fields">
|
||||||
|
<app-form-field
|
||||||
|
[errorsDictionary]="companyAndNameErrorsDictionary"
|
||||||
|
formControlName="name"
|
||||||
|
[label]="(languageManager.strings.name|upperfirst) + ':'"
|
||||||
|
[placeholder]="languageManager.strings.contactsName|upperfirst"
|
||||||
|
/>
|
||||||
|
<app-form-field
|
||||||
|
[errorsDictionary]="companyAndNameErrorsDictionary"
|
||||||
|
formControlName="company"
|
||||||
|
[label]="(languageManager.strings.company|upperfirst) + ':'"
|
||||||
|
[placeholder]="languageManager.strings.contactsCompany|upperfirst"
|
||||||
|
/>
|
||||||
|
<app-form-field
|
||||||
|
[errorsDictionary]="phoneErrorsDictionary"
|
||||||
|
formControlName="phone"
|
||||||
|
[label]="(languageManager.strings.phone|upperfirst) +':'"
|
||||||
|
[placeholder]="languageManager.strings.contactsPhone|upperfirst"
|
||||||
|
type="tel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<app-squared-btn
|
||||||
|
[text]="submitText()"
|
||||||
|
></app-squared-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
27
src/app/components/contact-form/contact-form.scss
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
|
app-squared-btn {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
app-form-field {
|
||||||
|
flex: 0 0 calc(33.33% - 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
app-squared-btn {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/app/components/contact-form/contact-form.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ContactForm } from './contact-form';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('ContactForm', () => {
|
||||||
|
let component: ContactForm;
|
||||||
|
let fixture: ComponentFixture<ContactForm>;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, [], { strings: strings.en });
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContactForm],
|
||||||
|
providers: [{ provide: LanguageManager, useValue: languageManager }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ContactForm);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle submit', () => {
|
||||||
|
const NAME_MOCK = 'Gabriel';
|
||||||
|
const COMPANY_MOCK = 'Gabilandia';
|
||||||
|
const PHONE_MOCK = '+5491123873991';
|
||||||
|
const emitSpy = spyOn(component.contact, 'emit');
|
||||||
|
const nameInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="name"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
const companyInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="company"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
const phoneInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="phone"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
nameInput.triggerEventHandler('input', { target: { value: NAME_MOCK } });
|
||||||
|
companyInput.triggerEventHandler('input', { target: { value: COMPANY_MOCK } });
|
||||||
|
phoneInput.triggerEventHandler('input', { target: { value: PHONE_MOCK } });
|
||||||
|
const submitBtn = fixture.debugElement.query(By.css('app-squared-btn')).query(By.css('button'));
|
||||||
|
(<HTMLButtonElement>submitBtn.nativeElement).click();
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(
|
||||||
|
new ContactDTO(undefined, NAME_MOCK, COMPANY_MOCK, PHONE_MOCK)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call handleSubmit if any field is null', () => {
|
||||||
|
const NAME_MOCK = 'Gabriel';
|
||||||
|
const COMPANY_MOCK = 'Gabilandia';
|
||||||
|
// Phone is null
|
||||||
|
const emitSpy = spyOn(component.contact, 'emit');
|
||||||
|
const nameInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="name"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
const companyInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="company"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
|
||||||
|
nameInput.triggerEventHandler('input', { target: { value: NAME_MOCK } });
|
||||||
|
companyInput.triggerEventHandler('input', { target: { value: COMPANY_MOCK } });
|
||||||
|
|
||||||
|
const submitBtn = fixture.debugElement.query(By.css('app-squared-btn')).query(By.css('button'));
|
||||||
|
(<HTMLButtonElement>submitBtn.nativeElement).click();
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
39
src/app/components/contact-form/contact-form.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, inject, input, output } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { FormField } from '../form-field/form-field';
|
||||||
|
import { SquaredBtn } from '../squared-btn/squared-btn';
|
||||||
|
import { NameAndCompanyFieldsErrorsDictionary } from '../../errors-dictionaries/name-and-company-field';
|
||||||
|
import { PhoneFieldErroresDictionary } from '../../errors-dictionaries/phone-field';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { ContactFormValue } from '../../types/ContactFormValue.type';
|
||||||
|
import { FormGroupContact } from '../../utils/form-group-contact';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-contact-form',
|
||||||
|
imports: [FormField, ReactiveFormsModule, SquaredBtn, UpperfirstPipe],
|
||||||
|
templateUrl: './contact-form.html',
|
||||||
|
styleUrl: './contact-form.scss',
|
||||||
|
})
|
||||||
|
export class ContactForm {
|
||||||
|
contact = output<ContactDTO>();
|
||||||
|
form = input(new FormGroupContact());
|
||||||
|
submitText = input('');
|
||||||
|
protected languageManager = inject(LanguageManager);
|
||||||
|
protected companyAndNameErrorsDictionary =
|
||||||
|
new NameAndCompanyFieldsErrorsDictionary().getDictionary();
|
||||||
|
protected phoneErrorsDictionary = new PhoneFieldErroresDictionary().getDictionary();
|
||||||
|
|
||||||
|
handleSubmit(contactForm: ContactFormValue) {
|
||||||
|
if (contactForm.company === null || contactForm.name === null || contactForm.phone === null)
|
||||||
|
return;
|
||||||
|
const contact = new ContactDTO(
|
||||||
|
undefined,
|
||||||
|
contactForm.name,
|
||||||
|
contactForm.company,
|
||||||
|
contactForm.phone
|
||||||
|
);
|
||||||
|
this.contact.emit(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div>
|
||||||
|
<table class="contact-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{languageManager.strings.name}}</th>
|
||||||
|
<th>{{languageManager.strings.company}}</th>
|
||||||
|
<th>{{languageManager.strings.phone}}</th>
|
||||||
|
<th>{{languageManager.strings.actions}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for(contact of contactList(); track contact.id ) {
|
||||||
|
<tr [style.display]="'table-row'">
|
||||||
|
<td>{{contact.name}}</td>
|
||||||
|
<td>{{contact.company}}</td>
|
||||||
|
<td>{{contact.phone}}</td>
|
||||||
|
<td><app-contact-actions-bar (editContact)="edit(contact.id)" (deleteContact)="delete(contact.id)"/></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
div:first-of-type {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.contact-list {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& thead {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
& th {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& tbody td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
&:nth-child(4) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ContactListTable } from './contact-list-table';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('ContactListTable', () => {
|
||||||
|
let component: ContactListTable;
|
||||||
|
let fixture: ComponentFixture<ContactListTable>;
|
||||||
|
|
||||||
|
let contactService: jasmine.SpyObj<ContactService>;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
let CONTACT_LIST_MOCK: ContactDTO[];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
contactService = jasmine.createSpyObj(ContactService.name, ['delete']);
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, [], { strings: strings.en });
|
||||||
|
router = jasmine.createSpyObj(Router.name, ['navigate']);
|
||||||
|
CONTACT_LIST_MOCK = [new ContactDTO(1, 'MOCK', 'MOCK', '5491122222222')];
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContactListTable],
|
||||||
|
providers: [
|
||||||
|
{ provide: ContactService, useValue: contactService },
|
||||||
|
{ provide: LanguageManager, useValue: languageManager },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
contactService.delete.and.returnValue(of([]));
|
||||||
|
fixture = TestBed.createComponent(ContactListTable);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.componentRef.setInput('contactList', CONTACT_LIST_MOCK);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should call delete for valid ID', () => {
|
||||||
|
const deleteSpy = spyOn(component, 'delete').and.callThrough();
|
||||||
|
const deleteButton = fixture.debugElement
|
||||||
|
.query(By.css('app-contact-actions-bar'))
|
||||||
|
.query(By.css('.btn--delete'));
|
||||||
|
deleteButton.triggerEventHandler('click');
|
||||||
|
expect(deleteSpy).toHaveBeenCalledWith(CONTACT_LIST_MOCK[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't call delete method if ID is falsy", () => {
|
||||||
|
CONTACT_LIST_MOCK[0].id = undefined;
|
||||||
|
const deleteButton = fixture.debugElement
|
||||||
|
.query(By.css('app-contact-actions-bar'))
|
||||||
|
.query(By.css('.btn--delete'));
|
||||||
|
deleteButton.triggerEventHandler('click');
|
||||||
|
expect(contactService.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edit', () => {
|
||||||
|
it("shouldn't navigate if ID is falsy", () => {
|
||||||
|
CONTACT_LIST_MOCK[0].id = undefined;
|
||||||
|
const editButton = fixture.debugElement
|
||||||
|
.query(By.css('app-contact-actions-bar'))
|
||||||
|
.query(By.css('.btn--edit'));
|
||||||
|
editButton.triggerEventHandler('click');
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call navigate for valid ID', () => {
|
||||||
|
const editButton = fixture.debugElement
|
||||||
|
.query(By.css('app-contact-actions-bar'))
|
||||||
|
.query(By.css('.btn--edit'));
|
||||||
|
editButton.triggerEventHandler('click');
|
||||||
|
expect(router.navigate).toHaveBeenCalledOnceWith(['edit', CONTACT_LIST_MOCK[0].id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
src/app/components/contact-list-table/contact-list-table.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component, inject, input } from '@angular/core';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { ContactActionsBar } from '../contact-actions-bar/contact-actions-bar';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-contact-list-table',
|
||||||
|
imports: [ContactActionsBar],
|
||||||
|
templateUrl: './contact-list-table.html',
|
||||||
|
styleUrl: './contact-list-table.scss',
|
||||||
|
})
|
||||||
|
export class ContactListTable {
|
||||||
|
contactList = input<ContactDTO[]>([]);
|
||||||
|
protected readonly languageManager = inject(LanguageManager);
|
||||||
|
private readonly contactService = inject(ContactService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
edit(id?: number) {
|
||||||
|
if (!id) return;
|
||||||
|
this.router.navigate(['edit', id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id?: number) {
|
||||||
|
if (!id) return;
|
||||||
|
this.contactService.delete(id).subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/app/components/contact-list/contact-list.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="contact-list">
|
||||||
|
<div class="contact-list__container">
|
||||||
|
<h2>{{languageManager.strings.contacts|upperfirst}}</h2>
|
||||||
|
<app-contact-search-bar (contactSearch)="filter=$event"/>
|
||||||
|
@let contactsCount = (this.contacts$|async|contactsFilter:filter)?.length ?? 0;
|
||||||
|
<app-counter
|
||||||
|
[count]="contactsCount"
|
||||||
|
item="{{languageManager.strings.contacts}}"
|
||||||
|
/>
|
||||||
|
@if(contactsCount !== 0) {
|
||||||
|
<app-contact-list-table [contactList]="(this.contacts$|async)|contactsFilter:filter"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
7
src/app/components/contact-list/contact-list.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.contact-list {
|
||||||
|
padding: 2rem;
|
||||||
|
&__container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 4rem auto 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/app/components/contact-list/contact-list.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ContactList } from './contact-list';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('ContactList', () => {
|
||||||
|
let component: ContactList;
|
||||||
|
let fixture: ComponentFixture<ContactList>;
|
||||||
|
let contactService: jasmine.SpyObj<ContactService>;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
contactService = jasmine.createSpyObj(ContactService.name, ['getAll']);
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, [], { strings: strings.en });
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContactList],
|
||||||
|
providers: [
|
||||||
|
{ provide: ContactService, useValue: contactService },
|
||||||
|
{ provide: LanguageManager, useValue: languageManager },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
contactService.getAll.and.returnValue(of([]));
|
||||||
|
fixture = TestBed.createComponent(ContactList);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/app/components/contact-list/contact-list.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component, inject, OnInit } from '@angular/core';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
import { ContactSearchBar } from '../contact-search-bar/contact-search-bar';
|
||||||
|
import { ContactListTable } from '../contact-list-table/contact-list-table';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
import { Counter } from '../counter/counter';
|
||||||
|
import { ContactsFilterPipe } from '../../pipes/contacts-filter-pipe';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-contact-list',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
UpperfirstPipe,
|
||||||
|
ContactSearchBar,
|
||||||
|
ContactListTable,
|
||||||
|
Counter,
|
||||||
|
ContactsFilterPipe,
|
||||||
|
],
|
||||||
|
templateUrl: './contact-list.html',
|
||||||
|
styleUrl: './contact-list.scss',
|
||||||
|
})
|
||||||
|
export class ContactList implements OnInit {
|
||||||
|
private readonly contactService = inject(ContactService);
|
||||||
|
protected readonly languageManager = inject(LanguageManager);
|
||||||
|
protected contacts$ = this.contactService.contacts$;
|
||||||
|
protected filter = '';
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.contactService.getAll().subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<input
|
||||||
|
(input)="contactSearch.emit($event.target.value)"
|
||||||
|
class="search-bar shadow"
|
||||||
|
placeholder="{{languageManager.strings.searchContactPlaceholder|upperfirst}}"
|
||||||
|
type="text">
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.search-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
-webkit-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
-moz-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ContactSearchBar } from './contact-search-bar';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('ContactSearchBar', () => {
|
||||||
|
let component: ContactSearchBar;
|
||||||
|
let fixture: ComponentFixture<ContactSearchBar>;
|
||||||
|
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, [], { strings: strings.en });
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContactSearchBar],
|
||||||
|
providers: [{ provide: LanguageManager, useValue: languageManager }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ContactSearchBar);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
14
src/app/components/contact-search-bar/contact-search-bar.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component, inject, output } from '@angular/core';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-contact-search-bar',
|
||||||
|
imports: [UpperfirstPipe],
|
||||||
|
templateUrl: './contact-search-bar.html',
|
||||||
|
styleUrl: './contact-search-bar.scss',
|
||||||
|
})
|
||||||
|
export class ContactSearchBar {
|
||||||
|
contactSearch = output<string>();
|
||||||
|
protected readonly languageManager = inject(LanguageManager);
|
||||||
|
}
|
||||||
1
src/app/components/counter/counter.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p class="total-elements"><span>{{count()}}</span> {{item()|upperfirst}}</p>
|
||||||
17
src/app/components/counter/counter.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-elements {
|
||||||
|
font-family: var(--secondaryFont);
|
||||||
|
margin: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/app/components/counter/counter.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Counter } from './counter';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
|
||||||
|
describe('Counter', () => {
|
||||||
|
let component: Counter;
|
||||||
|
let fixture: ComponentFixture<Counter>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Counter, UpperfirstPipe]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Counter);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/app/components/counter/counter.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { UpperfirstPipe } from "../../pipes/upperfirst-pipe";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-counter',
|
||||||
|
imports: [UpperfirstPipe],
|
||||||
|
templateUrl: './counter.html',
|
||||||
|
styleUrl: './counter.scss',
|
||||||
|
})
|
||||||
|
export class Counter {
|
||||||
|
item = input('');
|
||||||
|
count = input(0);
|
||||||
|
}
|
||||||
19
src/app/components/form-field/form-field.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="form-field">
|
||||||
|
<label [for]="control.name">{{label()}}</label>
|
||||||
|
<input
|
||||||
|
(input)="onChange($event)"
|
||||||
|
(blur)="onTouchedFn()"
|
||||||
|
[disabled]="isDisabled()"
|
||||||
|
[id]="control.name"
|
||||||
|
[value]="control.value"
|
||||||
|
placeholder="{{placeholder()}}"
|
||||||
|
type="{{type()}}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
@for(error of errorsDictionary()|keyvalue; track error.key) {
|
||||||
|
@if(control.hasError(error.key) && isDirty){
|
||||||
|
<p class="error-text">• {{error.value}}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
30
src/app/components/form-field/form-field.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@media (min-width: 768px) {
|
||||||
|
.form-field {
|
||||||
|
flex: 0 0 calc(33.33% - 1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='tel'] {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem;
|
||||||
|
height: 3rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
margin: 0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
47
src/app/components/form-field/form-field.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormField } from './form-field';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Component, viewChild } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'form-mock',
|
||||||
|
imports: [ReactiveFormsModule, FormField],
|
||||||
|
template: ` <form [formGroup]="form">
|
||||||
|
<app-form-field formControlName="mock"/>
|
||||||
|
</form>`,
|
||||||
|
})
|
||||||
|
class FormMock {
|
||||||
|
formField = viewChild.required<FormField>(FormField);
|
||||||
|
form = new FormGroup({
|
||||||
|
mock: new FormControl(),
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FormField', () => {
|
||||||
|
let component: FormMock;
|
||||||
|
let fixture: ComponentFixture<FormMock>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FormField, ReactiveFormsModule],
|
||||||
|
}).compileComponents();
|
||||||
|
fixture = TestBed.createComponent(FormMock);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onChanges', () => {
|
||||||
|
const onChangeSpy = spyOn(component.formField(), 'onChange').and.callThrough();
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('input', {target: {value: 'a'}});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(onChangeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
53
src/app/components/form-field/form-field.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { KeyValuePipe } from '@angular/common';
|
||||||
|
import { Component, computed, input, Optional, Self, signal } from '@angular/core';
|
||||||
|
import { ControlValueAccessor, NgControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-form-field',
|
||||||
|
imports: [ReactiveFormsModule, KeyValuePipe],
|
||||||
|
templateUrl: './form-field.html',
|
||||||
|
styleUrl: './form-field.scss',
|
||||||
|
})
|
||||||
|
export class FormField implements ControlValueAccessor {
|
||||||
|
readonly disabled = input(false);
|
||||||
|
readonly errorsDictionary = input<{ [key: string]: string }>({});
|
||||||
|
readonly formDisabled = signal(false);
|
||||||
|
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
|
||||||
|
readonly label = input('');
|
||||||
|
readonly type = input('text');
|
||||||
|
placeholder = input('');
|
||||||
|
|
||||||
|
onChangeFn: any = () => {};
|
||||||
|
onTouchedFn: any = () => {};
|
||||||
|
|
||||||
|
constructor(@Self() @Optional() protected readonly control: NgControl) {
|
||||||
|
if (this.control === null) {
|
||||||
|
console.warn('Form control is null, did you forget to add the formGroup?');
|
||||||
|
}
|
||||||
|
this.control.valueAccessor = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(obj: any): void {}
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.onChangeFn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
this.onTouchedFn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
this.formDisabled.set(isDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(event: Event): void {
|
||||||
|
const data = (event.target as HTMLInputElement).value;
|
||||||
|
this.onChangeFn(data);
|
||||||
|
this.onTouchedFn();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDirty() {
|
||||||
|
return this.control.dirty === null ? false : this.control.dirty;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/app/components/form-header/form-header.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<legend>{{title()}}
|
||||||
|
@if(subtitle()) {
|
||||||
|
<span>{{subtitle()}}</span>
|
||||||
|
}
|
||||||
|
</legend>
|
||||||
13
src/app/components/form-header/form-header.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
legend {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--secondaryFont);
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
span {
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/components/form-header/form-header.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormHeader } from './form-header';
|
||||||
|
|
||||||
|
describe('FormHeader', () => {
|
||||||
|
let component: FormHeader;
|
||||||
|
let fixture: ComponentFixture<FormHeader>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FormHeader]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FormHeader);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/app/components/form-header/form-header.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-form-header',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './form-header.html',
|
||||||
|
styleUrl: './form-header.scss',
|
||||||
|
})
|
||||||
|
export class FormHeader {
|
||||||
|
title = input('');
|
||||||
|
subtitle = input('');
|
||||||
|
}
|
||||||
5
src/app/components/lang-switch-btn/lang-switch-btn.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<button class="container" (click)="updateLanguage()" (keypress)="updateLanguage()">
|
||||||
|
<img
|
||||||
|
[src]="flagSrc()"
|
||||||
|
alt="">
|
||||||
|
</button>
|
||||||
19
src/app/components/lang-switch-btn/lang-switch-btn.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.container {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
min-height: 65px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
img {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
min-height: 76.8px;
|
||||||
|
img {
|
||||||
|
width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/components/lang-switch-btn/lang-switch-btn.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LangSwitchBtn } from './lang-switch-btn';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('LangSwitchBtn', () => {
|
||||||
|
let component: LangSwitchBtn;
|
||||||
|
let fixture: ComponentFixture<LangSwitchBtn>;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager, ['selectedLanguage$', 'setLanguage']);
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LangSwitchBtn],
|
||||||
|
providers: [{ provide: LanguageManager, useValue: languageManager }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LangSwitchBtn);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show spanish flag', () => {
|
||||||
|
languageManager.selectedLanguage$.and.returnValue('en');
|
||||||
|
fixture.detectChanges();
|
||||||
|
const img = fixture.debugElement.query(By.css('img')).attributes;
|
||||||
|
expect(img['src']).toEqual('images/sp_flag.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should updateLanguage on click', () => {
|
||||||
|
const updateLanguageSpy = spyOn(component, 'updateLanguage').and.callThrough();
|
||||||
|
languageManager.selectedLanguage$.and.returnValue('en');
|
||||||
|
fixture.detectChanges();
|
||||||
|
const container = fixture.debugElement.query(By.css('.container'));
|
||||||
|
container.triggerEventHandler('click');
|
||||||
|
expect(updateLanguageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/app/components/lang-switch-btn/lang-switch-btn.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component, computed, inject } from '@angular/core';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-lang-switch-btn',
|
||||||
|
templateUrl: './lang-switch-btn.html',
|
||||||
|
styleUrl: './lang-switch-btn.scss',
|
||||||
|
})
|
||||||
|
export class LangSwitchBtn {
|
||||||
|
protected readonly languageManager = inject(LanguageManager);
|
||||||
|
|
||||||
|
flagSrc = computed(() => {
|
||||||
|
if (this.languageManager.selectedLanguage$() === 'en') {
|
||||||
|
return 'images/sp_flag.png';
|
||||||
|
} else {
|
||||||
|
return 'images/uk_flag.png';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateLanguage() {
|
||||||
|
this.languageManager.setLanguage(
|
||||||
|
this.languageManager.selectedLanguage$() === 'en' ? 'es' : 'en'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/app/components/main-header/main-header.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="bar-container">
|
||||||
|
<div class="bar-container__elements">
|
||||||
|
<div class="bar-container__action-button bar-container__action-button--left">
|
||||||
|
<ng-content select="[slot='action-button-left']"></ng-content>
|
||||||
|
</div>
|
||||||
|
<h1 class="bar-container__title">{{title()|titlecase}}</h1>
|
||||||
|
<div class="bar-container__action-button bar-container__action-button--right">
|
||||||
|
<ng-content select="[slot='action-button-right']"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
50
src/app/components/main-header/main-header.scss
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.bar-container {
|
||||||
|
background-color: var(--primaryDark);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__elements {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
&--left,
|
||||||
|
&--right {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
&--left {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
&--right {
|
||||||
|
margin-right: 8px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
&__title {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
&__action-button {
|
||||||
|
&--left {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
&--right {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/components/main-header/main-header.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MainHeader } from './main-header';
|
||||||
|
|
||||||
|
describe('MainHeader', () => {
|
||||||
|
let component: MainHeader;
|
||||||
|
let fixture: ComponentFixture<MainHeader>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MainHeader]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MainHeader);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/app/components/main-header/main-header.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { TitleCasePipe } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-header',
|
||||||
|
imports: [TitleCasePipe],
|
||||||
|
templateUrl: './main-header.html',
|
||||||
|
styleUrl: './main-header.scss',
|
||||||
|
})
|
||||||
|
export class MainHeader {
|
||||||
|
title = input('');
|
||||||
|
}
|
||||||
9
src/app/components/notification/notification.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@if(notification$(); as notification) {
|
||||||
|
<div class="notification notification--{{notification.type}} notification--fade-in-out">
|
||||||
|
<p>{{ notification.message|upperfirst }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
43
src/app/components/notification/notification.scss
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
@keyframes fade-in-out {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
-moz-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
-webkit-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
position: fixed;
|
||||||
|
right: 1rem;
|
||||||
|
top: 1rem;
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
&--fade-in-out {
|
||||||
|
animation: fade-in-out ease-in-out 1s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background-color: rgb(179, 241, 117);
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background-color: rgb(238, 148, 166);
|
||||||
|
color: rgb(163, 0, 33);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/components/notification/notification.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Notification } from './notification';
|
||||||
|
|
||||||
|
describe('Notification', () => {
|
||||||
|
let component: Notification;
|
||||||
|
let fixture: ComponentFixture<Notification>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Notification],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Notification);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
14
src/app/components/notification/notification.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { Notifier } from '../../services/notifier';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-notification',
|
||||||
|
imports: [UpperfirstPipe],
|
||||||
|
templateUrl: './notification.html',
|
||||||
|
styleUrl: './notification.scss',
|
||||||
|
})
|
||||||
|
export class Notification {
|
||||||
|
private readonly notificationService = inject(Notifier);
|
||||||
|
protected readonly notification$ = this.notificationService.notification$;
|
||||||
|
}
|
||||||
1
src/app/components/rounded-btn/rounded-btn.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<button class="rounded-btn">{{text()}}</button>
|
||||||
19
src/app/components/rounded-btn/rounded-btn.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-btn {
|
||||||
|
background-color: var(--secondary);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/components/rounded-btn/rounded-btn.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RoundedBtn } from './rounded-btn';
|
||||||
|
|
||||||
|
describe('RoundedBtn', () => {
|
||||||
|
let component: RoundedBtn;
|
||||||
|
let fixture: ComponentFixture<RoundedBtn>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RoundedBtn]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RoundedBtn);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/app/components/rounded-btn/rounded-btn.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-rounded-btn',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './rounded-btn.html',
|
||||||
|
styleUrl: './rounded-btn.scss',
|
||||||
|
})
|
||||||
|
export class RoundedBtn {
|
||||||
|
text = input('');
|
||||||
|
}
|
||||||
3
src/app/components/squared-btn/squared-btn.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<button class="squared-btn">
|
||||||
|
{{text()}}
|
||||||
|
</button>
|
||||||
16
src/app/components/squared-btn/squared-btn.scss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.squared-btn {
|
||||||
|
background-color: var(--primary);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 1rem 4rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: background-color 0.5s ease-in-out;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primaryDark);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
src/app/components/squared-btn/squared-btn.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SquaredBtn } from './squared-btn';
|
||||||
|
|
||||||
|
describe('SquaredBtn', () => {
|
||||||
|
let component: SquaredBtn;
|
||||||
|
let fixture: ComponentFixture<SquaredBtn>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SquaredBtn]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SquaredBtn);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/app/components/squared-btn/squared-btn.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-squared-btn',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './squared-btn.html',
|
||||||
|
styleUrl: './squared-btn.scss',
|
||||||
|
})
|
||||||
|
export class SquaredBtn {
|
||||||
|
text = input('');
|
||||||
|
}
|
||||||
18
src/app/errors-dictionaries/name-and-company-field.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Dictionary } from '../interfaces/dictionary.interface';
|
||||||
|
import { LanguageManager } from '../services/language-manager';
|
||||||
|
|
||||||
|
export class NameAndCompanyFieldsErrorsDictionary implements Dictionary {
|
||||||
|
private readonly languageManager = inject(LanguageManager);
|
||||||
|
private readonly maxlen: string;
|
||||||
|
private readonly required = this.languageManager.strings.errorMessageRequired;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.maxlen = this.languageManager.strings.errorMessageMaxLength(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDictionary(): { [key: string]: string } {
|
||||||
|
const { maxlen, required } = this;
|
||||||
|
return { maxlen, required };
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/app/errors-dictionaries/phone-field.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Dictionary } from '../interfaces/dictionary.interface';
|
||||||
|
import { LanguageManager } from '../services/language-manager';
|
||||||
|
|
||||||
|
export class PhoneFieldErroresDictionary implements Dictionary {
|
||||||
|
languageManager = inject(LanguageManager);
|
||||||
|
pattern = this.languageManager.strings.errorMessagePhonePattern;
|
||||||
|
required = this.languageManager.strings.errorMessageRequired;
|
||||||
|
|
||||||
|
getDictionary(): { [key: string]: string } {
|
||||||
|
const { required, pattern } = this;
|
||||||
|
return { required, pattern };
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/app/interfaces/dictionary.interface.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface Dictionary {
|
||||||
|
getDictionary(): { [key: string]: string };
|
||||||
|
}
|
||||||
3
src/app/models/ContactDTO.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export class ContactDTO {
|
||||||
|
constructor(public id?: number, public name = "", public company = "", public phone = ""){}
|
||||||
|
}
|
||||||
3
src/app/models/Notification.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export class Notification {
|
||||||
|
constructor(public message = '', public type: 'success' | 'error' = 'success') {}
|
||||||
|
}
|
||||||
8
src/app/models/Response.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { ResponseError } from './ResponseError';
|
||||||
|
|
||||||
|
export interface Response<T> {
|
||||||
|
data: T;
|
||||||
|
errors?: ResponseError[];
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
6
src/app/models/ResponseError.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ResponseError {
|
||||||
|
code: number;
|
||||||
|
details: string;
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
93
src/app/pages/edit/contact-resolver.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRouteSnapshot, ResolveFn, Router } from '@angular/router';
|
||||||
|
import { contactResolver } from './contact-resolver';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('contactResolver', () => {
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
let ROUTE_MOCK: jasmine.SpyObj<ActivatedRouteSnapshot>;
|
||||||
|
|
||||||
|
const executeResolver: ResolveFn<Observable<ContactDTO | null>> = (...resolverParameters) =>
|
||||||
|
TestBed.runInInjectionContext(() => contactResolver(...resolverParameters));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, [], { strings: strings.en });
|
||||||
|
router = jasmine.createSpyObj(Router.name, ['navigate']);
|
||||||
|
ROUTE_MOCK = jasmine.createSpyObj(ActivatedRouteSnapshot.name, [], {
|
||||||
|
paramMap: {
|
||||||
|
get() {
|
||||||
|
return '1';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
provideHttpClient(),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: LanguageManager, useValue: languageManager },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(executeResolver).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate back on error response', () => {
|
||||||
|
(
|
||||||
|
executeResolver(ROUTE_MOCK, {
|
||||||
|
url: '',
|
||||||
|
root: new ActivatedRouteSnapshot(),
|
||||||
|
}) as Observable<ContactDTO | null>
|
||||||
|
).subscribe();
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiUrl}/contacts/${ROUTE_MOCK.paramMap.get('id')}`
|
||||||
|
);
|
||||||
|
expect(req.request.method).toEqual('GET');
|
||||||
|
req.flush(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
code: 4400,
|
||||||
|
message: 'Resource not found',
|
||||||
|
details: 'Contact not found',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ status: 404, statusText: 'Resource not found' }
|
||||||
|
);
|
||||||
|
expect(router.navigate).toHaveBeenCalledOnceWith(['/']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return and observable with the fetched contact', () => {
|
||||||
|
(
|
||||||
|
executeResolver(ROUTE_MOCK, {
|
||||||
|
url: '',
|
||||||
|
root: new ActivatedRouteSnapshot(),
|
||||||
|
}) as Observable<ContactDTO | null>
|
||||||
|
).subscribe();
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiUrl}/contacts/${ROUTE_MOCK.paramMap.get('id')}`
|
||||||
|
);
|
||||||
|
expect(req.request.method).toEqual('GET');
|
||||||
|
req.flush({
|
||||||
|
data: new ContactDTO(1, 'mock', 'mock', 'mock'),
|
||||||
|
success: true,
|
||||||
|
message: 'Contact retrieved successfully',
|
||||||
|
});
|
||||||
|
httpTestingController.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/app/pages/edit/contact-resolver.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { ResolveFn, Router } from '@angular/router';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { catchError, map, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
export const contactResolver: ResolveFn<Observable<ContactDTO | null>> = (route, _) => {
|
||||||
|
const contactService = inject(ContactService);
|
||||||
|
const router = inject(Router);
|
||||||
|
const contactId = route.paramMap.get('id') ?? '0';
|
||||||
|
|
||||||
|
return contactService.findById(contactId).pipe(
|
||||||
|
map((response) => response.data),
|
||||||
|
catchError(() => {
|
||||||
|
router.navigate(['/']);
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
24
src/app/pages/edit/edit.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<app-main-header
|
||||||
|
[title]="languageManager.strings.editContact"
|
||||||
|
>
|
||||||
|
<app-rounded-btn
|
||||||
|
(keydown)="goBack()"
|
||||||
|
(click)="goBack()"
|
||||||
|
slot="action-button-left"
|
||||||
|
text={{languageManager.strings.goBack|upperfirst}}
|
||||||
|
/>
|
||||||
|
</app-main-header>
|
||||||
|
<app-card bgColor="var(--secondary)">
|
||||||
|
@if(form$|async; as form){
|
||||||
|
<app-contact-form
|
||||||
|
class="form"
|
||||||
|
(contact)="edit(form.value)"
|
||||||
|
[form]="form"
|
||||||
|
[submitText]="languageManager.strings.save"
|
||||||
|
><app-form-header
|
||||||
|
[title]="languageManager.strings.editTheContact|upperfirst"
|
||||||
|
slot="header"
|
||||||
|
/>
|
||||||
|
</app-contact-form>
|
||||||
|
}
|
||||||
|
</app-card>
|
||||||
8
src/app/pages/edit/edit.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
app-card {
|
||||||
|
margin-top: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
display: block;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
72
src/app/pages/edit/edit.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Edit } from './edit';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('Edit', () => {
|
||||||
|
let component: Edit;
|
||||||
|
let fixture: ComponentFixture<Edit>;
|
||||||
|
|
||||||
|
let activatedRoute: jasmine.SpyObj<ActivatedRoute>;
|
||||||
|
let contactService: jasmine.SpyObj<ContactService>;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
let CONTACT_MOCK: ContactDTO;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
CONTACT_MOCK = new ContactDTO(1, 'mock', 'mock', 'mock');
|
||||||
|
activatedRoute = jasmine.createSpyObj(ActivatedRoute.name, [], {
|
||||||
|
data: of({ contact: CONTACT_MOCK }),
|
||||||
|
});
|
||||||
|
contactService = jasmine.createSpyObj(ContactService.name, ['update']);
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, [], { strings: strings.en });
|
||||||
|
router = jasmine.createSpyObj(Router.name, ['navigate']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Edit],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
|
{ provide: ContactService, useValue: contactService },
|
||||||
|
{ provide: LanguageManager, useValue: languageManager },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
TestBed.inject(ActivatedRoute);
|
||||||
|
fixture = TestBed.createComponent(Edit);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update on form submission', () => {
|
||||||
|
contactService.update.and.returnValue(
|
||||||
|
of({ success: true, data: null, message: 'Contact updated successfully' })
|
||||||
|
);
|
||||||
|
const MODIFICATION_MOCK = 'modified';
|
||||||
|
const nameInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="name"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
nameInput.triggerEventHandler('input', { target: { value: MODIFICATION_MOCK } });
|
||||||
|
CONTACT_MOCK.name = MODIFICATION_MOCK;
|
||||||
|
const submitBtn = fixture.debugElement.query(By.css('app-squared-btn')).query(By.css('button'));
|
||||||
|
(<HTMLButtonElement>submitBtn.nativeElement).click();
|
||||||
|
expect(contactService.update).toHaveBeenCalledOnceWith(CONTACT_MOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate back', () => {
|
||||||
|
const goBackBtn = fixture.debugElement.query(By.css('app-rounded-btn'));
|
||||||
|
goBackBtn.triggerEventHandler('click');
|
||||||
|
expect(router.navigate).toHaveBeenCalledOnceWith(['./..'], { relativeTo: activatedRoute });
|
||||||
|
});
|
||||||
|
});
|
||||||
56
src/app/pages/edit/edit.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { MainHeader } from '../../components/main-header/main-header';
|
||||||
|
import { RoundedBtn } from '../../components/rounded-btn/rounded-btn';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Card } from '../../components/card/card';
|
||||||
|
import { ContactForm } from '../../components/contact-form/contact-form';
|
||||||
|
import { FormHeader } from '../../components/form-header/form-header';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
import { map, Observable, take, tap } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { FormGroupContact } from '../../utils/form-group-contact';
|
||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { ContactFormValue } from '../../types/ContactFormValue.type';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit',
|
||||||
|
imports: [MainHeader, RoundedBtn, Card, ContactForm, FormHeader, UpperfirstPipe, AsyncPipe],
|
||||||
|
templateUrl: './edit.html',
|
||||||
|
styleUrl: './edit.scss',
|
||||||
|
})
|
||||||
|
export class Edit {
|
||||||
|
protected readonly languageManager = inject(LanguageManager);
|
||||||
|
private readonly activatedRoute = inject(ActivatedRoute);
|
||||||
|
private readonly contactService = inject(ContactService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
protected contactId?: number;
|
||||||
|
protected readonly form$ = (<Observable<{ contact: ContactDTO }>>this.activatedRoute.data).pipe(
|
||||||
|
take(1),
|
||||||
|
map((data) => data.contact),
|
||||||
|
tap((contact) => (this.contactId = contact.id)),
|
||||||
|
map((contact) => new FormGroupContact(contact.name, contact.company, contact.phone))
|
||||||
|
);
|
||||||
|
|
||||||
|
edit(form: ContactFormValue) {
|
||||||
|
if (this.contactId) {
|
||||||
|
const contact = new ContactDTO(
|
||||||
|
this.contactId,
|
||||||
|
form.name ?? '',
|
||||||
|
form.company ?? '',
|
||||||
|
form.phone ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
this.contactService
|
||||||
|
.update(contact)
|
||||||
|
.pipe(tap(() => this.goBack()))
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.router.navigate(['./..'], { relativeTo: this.activatedRoute });
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/pages/main/main.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<app-main-header [title]="languageManager.strings.contactAgenda">
|
||||||
|
<app-lang-switch-btn slot="action-button-right" style="z-index: 2;"/>
|
||||||
|
</app-main-header>
|
||||||
|
<app-card bgColor="var(--secondary)">
|
||||||
|
<app-contact-form
|
||||||
|
(contact)="save($event)"
|
||||||
|
[form]="form"
|
||||||
|
[submitText]="languageManager.strings.add"
|
||||||
|
class="form"
|
||||||
|
><app-form-header
|
||||||
|
[subtitle]="languageManager.strings.allFieldRequired|upperfirst"
|
||||||
|
[title]="languageManager.strings.addContact|upperfirst"
|
||||||
|
slot="header"
|
||||||
|
/>
|
||||||
|
</app-contact-form>
|
||||||
|
</app-card>
|
||||||
|
<app-card bgColor="#fff">
|
||||||
|
<app-contact-list/>
|
||||||
|
</app-card>
|
||||||
|
<footer></footer>
|
||||||
20
src/app/pages/main/main.scss
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
app-card {
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
margin-top: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-contact-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
52
src/app/pages/main/main.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Main } from './main';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { ContactList } from '../../components/contact-list/contact-list';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ContactForm } from '../../components/contact-form/contact-form';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
|
||||||
|
describe('Main', () => {
|
||||||
|
let component: Main;
|
||||||
|
let fixture: ComponentFixture<Main>;
|
||||||
|
let contactService: jasmine.SpyObj<ContactService>;
|
||||||
|
let languageManager: jasmine.SpyObj<LanguageManager>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
contactService = jasmine.createSpyObj(ContactList.name, ['getAll', 'save']);
|
||||||
|
contactService.getAll.and.returnValue(of([]));
|
||||||
|
languageManager = jasmine.createSpyObj(LanguageManager.name, ['selectedLanguage$'], {
|
||||||
|
strings: strings.en,
|
||||||
|
});
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Main],
|
||||||
|
providers: [
|
||||||
|
{ provide: ContactService, useValue: contactService },
|
||||||
|
{ provide: LanguageManager, useValue: languageManager },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
languageManager.selectedLanguage$.and.returnValue('en');
|
||||||
|
fixture = TestBed.createComponent(Main);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call save on contact event', () => {
|
||||||
|
contactService.save.and.returnValue(of([]));
|
||||||
|
const saveSpy = spyOn(component, 'save').and.callThrough();
|
||||||
|
const CONTACT_MOCK = new ContactDTO();
|
||||||
|
const contactForm: ContactForm = fixture.debugElement.query(
|
||||||
|
By.css('app-contact-form')
|
||||||
|
).componentInstance;
|
||||||
|
contactForm.contact.emit(CONTACT_MOCK);
|
||||||
|
expect(saveSpy).toHaveBeenCalledOnceWith(CONTACT_MOCK);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
src/app/pages/main/main.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { Card } from '../../components/card/card';
|
||||||
|
import { ContactForm } from '../../components/contact-form/contact-form';
|
||||||
|
import { ContactList } from '../../components/contact-list/contact-list';
|
||||||
|
import { ContactService } from '../../services/contact.service';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { FormHeader } from '../../components/form-header/form-header';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
import { MainHeader } from '../../components/main-header/main-header';
|
||||||
|
import { FormGroupContact } from '../../utils/form-group-contact';
|
||||||
|
import { LanguageManager } from '../../services/language-manager';
|
||||||
|
import { LangSwitchBtn } from '../../components/lang-switch-btn/lang-switch-btn';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main',
|
||||||
|
imports: [Card, ContactForm, ContactList, FormHeader, MainHeader, UpperfirstPipe, LangSwitchBtn],
|
||||||
|
templateUrl: './main.html',
|
||||||
|
styleUrl: './main.scss',
|
||||||
|
})
|
||||||
|
export class Main {
|
||||||
|
protected readonly form = new FormGroupContact();
|
||||||
|
protected readonly languageManager = inject(LanguageManager);
|
||||||
|
private readonly contactService = inject(ContactService);
|
||||||
|
|
||||||
|
save(contactDTO: ContactDTO) {
|
||||||
|
this.contactService.save(contactDTO).subscribe({
|
||||||
|
next: () => this.form.reset(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/app/pipes/contacts-filter-pipe.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { pipe } from 'rxjs';
|
||||||
|
import { ContactsFilterPipe } from './contacts-filter-pipe';
|
||||||
|
import { ContactDTO } from '../models/ContactDTO';
|
||||||
|
|
||||||
|
describe('ContactsFilterPipe', () => {
|
||||||
|
let pipe: ContactsFilterPipe;
|
||||||
|
let CONTACTS_MOCK = [
|
||||||
|
new ContactDTO(1,'mock')
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pipe = new ContactsFilterPipe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create an instance', () => {
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if value is null', () => {
|
||||||
|
expect(pipe.transform(null, '')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the passed array without transformation if name is empty (\'\')', () => {
|
||||||
|
expect(pipe.transform(CONTACTS_MOCK, '')).toEqual(CONTACTS_MOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return entries wich name contains the given string name', () => {
|
||||||
|
const ARG_MOCK = 'mo';
|
||||||
|
const transformationResult = pipe.transform(CONTACTS_MOCK, ARG_MOCK);
|
||||||
|
expect(transformationResult.find( c => c.name.includes(ARG_MOCK))).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/app/pipes/contacts-filter-pipe.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { ContactDTO } from '../models/ContactDTO';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'contactsFilter'
|
||||||
|
})
|
||||||
|
export class ContactsFilterPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(value: ContactDTO[]|null, name: string): ContactDTO[] {
|
||||||
|
if(value === null) return [];
|
||||||
|
if(name === '') return value;
|
||||||
|
return value.filter( contact => contact.name.toLowerCase().includes(name.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/app/pipes/upperfirst-pipe.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { UpperfirstPipe } from './upperfirst-pipe';
|
||||||
|
|
||||||
|
describe('UpperfirstPipe', () => {
|
||||||
|
let pipe: UpperfirstPipe;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pipe = new UpperfirstPipe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new UpperfirstPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uppercase only the first letter on a string', () => {
|
||||||
|
const unformattedString = "today is a great day";
|
||||||
|
const expectedString = "Today is a great day";
|
||||||
|
expect(pipe.transform(unformattedString)).toBe(expectedString);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
14
src/app/pipes/upperfirst-pipe.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'upperfirst'
|
||||||
|
})
|
||||||
|
export class UpperfirstPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(value: string) {
|
||||||
|
const upperFirst = value.charAt(0).toUpperCase();
|
||||||
|
value = value.slice(1);
|
||||||
|
return upperFirst + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
53
src/app/services/ResponseStateNotificatio.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ResponseStateNotification } from './ResponseStateNotificatio';
|
||||||
|
import { Notifier } from './notifier';
|
||||||
|
import { catchError, EMPTY, of, throwError } from 'rxjs';
|
||||||
|
import { Notification } from '../models/Notification';
|
||||||
|
|
||||||
|
describe('ResponseStateNotificatio', () => {
|
||||||
|
let service: ResponseStateNotification;
|
||||||
|
let notifier: jasmine.SpyObj<Notifier>;
|
||||||
|
|
||||||
|
let NOTIFICATION_MOCK: Notification;
|
||||||
|
beforeEach(() => {
|
||||||
|
notifier = jasmine.createSpyObj(Notifier.name, ['notify']);
|
||||||
|
NOTIFICATION_MOCK = new Notification('mock', 'success');
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [{ provide: Notifier, useValue: notifier }],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(ResponseStateNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify success if observable has no errors', () => {
|
||||||
|
service
|
||||||
|
.handleResponse(
|
||||||
|
of({
|
||||||
|
data: null,
|
||||||
|
message: 'success',
|
||||||
|
success: true,
|
||||||
|
}),
|
||||||
|
NOTIFICATION_MOCK.message,
|
||||||
|
NOTIFICATION_MOCK.message
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
expect(notifier.notify).toHaveBeenCalledOnceWith(NOTIFICATION_MOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify error if observable throws error', () => {
|
||||||
|
service
|
||||||
|
.handleResponse(
|
||||||
|
throwError(() => new Error('Stream errored!')),
|
||||||
|
NOTIFICATION_MOCK.message,
|
||||||
|
NOTIFICATION_MOCK.message
|
||||||
|
)
|
||||||
|
.pipe(catchError(() => of(EMPTY)))
|
||||||
|
.subscribe();
|
||||||
|
NOTIFICATION_MOCK.type = 'error';
|
||||||
|
expect(notifier.notify).toHaveBeenCalledOnceWith(NOTIFICATION_MOCK);
|
||||||
|
});
|
||||||
|
});
|
||||||