Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b3dadc18d | |||
| a788381eaa | |||
| a031c5cc99 | |||
| db1a56965f | |||
| 9677323597 | |||
| 56c6808508 | |||
| e5d4b748dc | |||
| 98eb72bd22 | |||
| 0dbd108051 | |||
| d63629ba5c | |||
| 696c3e77be | |||
| 73f92f6770 | |||
| 4d93f51767 | |||
| 0392ac49fb | |||
| fe919eaa35 | |||
| a57cd9c294 | |||
| 0accddaad9 | |||
| 15a0c8701c | |||
| d4dbfa58e9 | |||
| b87f0fc60a | |||
| 41733ec030 | |||
| 66cbd4b2d1 | |||
| 11b13e0bf4 | |||
| 3c4e182832 | |||
| 7ef616a2a9 | |||
| d3ccc2a46b | |||
| e0a235fcc5 | |||
| 25bc4e45ff | |||
| 2a51b4a2f3 | |||
| 729d019de9 | |||
| 430a69a95c | |||
| 0a53e68b54 | |||
| e061e0aadb | |||
| 6d78d60ecf | |||
| 3a0c062e78 | |||
| a5393f471f | |||
| 46bca919d7 | |||
| 741032ec02 | |||
| d63120b092 | |||
| 836a10e3ef | |||
| 8ca73b94aa | |||
| 447030533f | |||
| 66a93b2b97 | |||
| 6c71086104 | |||
| 4a2048c378 | |||
| f3e47126b3 | |||
| 09c2a350e4 | |||
| 92cd9be8ec | |||
| e6ead4937d | |||
| 5e17182686 | |||
| c52a5b8715 | |||
| a9829bf118 | |||
| d8e4cbfb9a | |||
| 0db55e9ba7 | |||
| 2534c67a5e | |||
| 6d88b87f93 | |||
| 64ed4876a5 | |||
| fd7a6bd1a1 | |||
| b250bfe76c | |||
| 63ac0231dc | |||
| 21533ee9f9 | |||
| 30f9829768 | |||
| a5b1d771a0 | |||
| b5425c62de | |||
| 0cc2561327 | |||
| f933312283 | |||
| a0d2bef4ef | |||
| eb8b283769 | |||
| fcfe9a2c08 | |||
| 9ee1a04a5c | |||
| 933974c958 | |||
| 5ff9d5a11b | |||
| 8b9313da93 | |||
| 9acdff19e1 | |||
| ad1118803d | |||
| 55a09d32b3 | |||
| 8b1b84b195 | |||
| acbf9c51a3 | |||
| 5bfea51b27 | |||
| a398866f04 | |||
| ee737aaf61 | |||
| c9460ac198 | |||
| 3c5f5e50e0 | |||
| b40b9538ab | |||
| 2fa77d3af6 |
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres
|
||||||
@@ -33,3 +32,7 @@ volumes:
|
|||||||
secrets:
|
secrets:
|
||||||
db-password:
|
db-password:
|
||||||
file: db-password.txt
|
file: db-password.txt
|
||||||
|
networks:
|
||||||
|
database:
|
||||||
|
name: database
|
||||||
|
external: true
|
||||||
|
|||||||
+97
-6
@@ -27,7 +27,7 @@ services:
|
|||||||
- integration
|
- integration
|
||||||
- frontend
|
- frontend
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8100:8100
|
||||||
volumes:
|
volumes:
|
||||||
- images-data:/data/images
|
- images-data:/data/images
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -41,14 +41,85 @@ services:
|
|||||||
- kontor-api:0.2.0-SNAPSHOT
|
- kontor-api:0.2.0-SNAPSHOT
|
||||||
image: kontor-api:0.2.0-SNAPSHOT
|
image: kontor-api:0.2.0-SNAPSHOT
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://kontor-api:8500/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8500:8500
|
||||||
|
volumes:
|
||||||
|
- images-data:/data/images
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-fiber:
|
||||||
|
build:
|
||||||
|
context: ./kontor-fiber
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-fiber:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-fiber:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://kontor-fiber:8600/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8600:8600
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-echo:
|
||||||
|
build:
|
||||||
|
context: ./kontor-echo
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-echo:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-echo:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://kontor-echo:8700/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8700:8700
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-quarkus:
|
||||||
|
build:
|
||||||
|
context: ./kontor-quarkus
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-quarkus:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-quarkus:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://kontor-quarkus:8800/q/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
networks:
|
networks:
|
||||||
- database
|
- database
|
||||||
- integration
|
- integration
|
||||||
- frontend
|
- frontend
|
||||||
ports:
|
ports:
|
||||||
- 8800:8800
|
- 8800:8800
|
||||||
volumes:
|
|
||||||
- images-data:/data/images
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -67,7 +138,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8200:80
|
- 8200:80
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
kontor-api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
kontor-vue:
|
kontor-vue:
|
||||||
build:
|
build:
|
||||||
@@ -86,13 +157,33 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
kontor-javalin:
|
||||||
|
build:
|
||||||
|
context: ./kontor-javalin
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-javalin:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-javalin:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://kontor-javalin:8400/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8400:8400
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
database:
|
|
||||||
integration:
|
integration:
|
||||||
name: integration
|
name: integration
|
||||||
frontend:
|
frontend:
|
||||||
volumes:
|
volumes:
|
||||||
activemq-data:
|
activemq-data:
|
||||||
images-data:
|
images-data:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# KontorAngular
|
# KontorAngular
|
||||||
|
|
||||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.1.1.
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.2.1.
|
||||||
|
|
||||||
## Development server
|
## Development server
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,7 @@
|
|||||||
"projects": {
|
"projects": {
|
||||||
"kontor-angular": {
|
"kontor-angular": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {},
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
@@ -18,8 +14,10 @@
|
|||||||
"builder": "@angular/build:application",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
@@ -27,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@@ -72,8 +70,11 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:karma",
|
"builder": "@angular/build:karma",
|
||||||
"options": {
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.css"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1418
-970
File diff suppressed because it is too large
Load Diff
+16
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kontor-angular",
|
"name": "kontor-angular",
|
||||||
"version": "0.2.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
"test": "ng test"
|
"test": "ng test"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": "*.html",
|
"files": "*.html",
|
||||||
@@ -20,26 +22,27 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^20.1.0",
|
"@angular/common": "^20.2.0",
|
||||||
"@angular/compiler": "^20.1.0",
|
"@angular/compiler": "^20.2.0",
|
||||||
"@angular/core": "^20.1.0",
|
"@angular/core": "^20.2.0",
|
||||||
"@angular/forms": "^20.1.0",
|
"@angular/forms": "^20.2.0",
|
||||||
"@angular/platform-browser": "^20.1.0",
|
"@angular/platform-browser": "^20.2.0",
|
||||||
"@angular/router": "^20.1.0",
|
"@angular/router": "^20.2.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.1.1",
|
"@angular/build": "^20.2.1",
|
||||||
"@angular/cli": "^20.1.1",
|
"@angular/cli": "^20.2.1",
|
||||||
"@angular/compiler-cli": "^20.1.0",
|
"@angular/compiler-cli": "^20.2.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.8.0",
|
"jasmine-core": "~5.9.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.8.2"
|
"typescript": "~5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 634 B |
@@ -0,0 +1,9 @@
|
|||||||
|
h1 {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
/* max-width: 500px; */
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="app">
|
||||||
|
<kontor-header />
|
||||||
|
<app-navigation/>
|
||||||
|
<main>
|
||||||
|
<router-outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<kontor-footer />
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { KontorHeaderComponent } from "./kontor/header/header.component";
|
||||||
|
import { KontorFooterComponent } from './kontor/footer/footer.component';
|
||||||
|
import { NavigationComponent } from "./kontor/navigation/navigation.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet, KontorHeaderComponent, KontorFooterComponent, NavigationComponent],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.css'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideZonelessChangeDetection(),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes)
|
provideRouter(routes, withComponentInputBinding()),
|
||||||
|
provideHttpClient(),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,342 +0,0 @@
|
|||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,26 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
import { KontorComponent } from './kontor/kontor.component';
|
||||||
|
import { Auth } from './common/auth/auth';
|
||||||
|
import { ComicSectionComponent } from './kontor/comic/comic-section/comic-section.component';
|
||||||
|
import { comicRoutes } from './kontor/comic/comic-section/comic-section.routes';
|
||||||
|
import { TyscSectionComponent } from './kontor/tysc/tysc-section/tysc-section.component';
|
||||||
|
import { tyscRoutes } from './kontor/tysc/tysc-section/tysc-section.routes';
|
||||||
|
import { MediaSectionComponent } from './kontor/media/media-section/media-section.component';
|
||||||
|
import { mediaRoutes } from './kontor/media/media-section/media-section.routes';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{ path: '', component: KontorComponent, },
|
||||||
|
{ path: 'login', component: Auth, },
|
||||||
|
{
|
||||||
|
path: 'comic', component: ComicSectionComponent,
|
||||||
|
children: comicRoutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tysc', component: TyscSectionComponent,
|
||||||
|
children: tyscRoutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'media', component: MediaSectionComponent,
|
||||||
|
children: mediaRoutes,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { provideZonelessChangeDetection } from '@angular/core';
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
|
|
||||||
@@ -6,7 +5,6 @@ describe('App', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [App],
|
imports: [App],
|
||||||
providers: [provideZonelessChangeDetection()]
|
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Component, signal } from '@angular/core';
|
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
imports: [RouterOutlet],
|
|
||||||
templateUrl: './app.html',
|
|
||||||
styleUrl: './app.scss'
|
|
||||||
})
|
|
||||||
export class App {
|
|
||||||
protected readonly title = signal('kontor-angular');
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthService } from './auth-service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { catchError, Observable, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
|
interface AuthResponseData {
|
||||||
|
kind: string;
|
||||||
|
idToken: string;
|
||||||
|
email: string;
|
||||||
|
refreshToken: string;
|
||||||
|
expiresIn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenData {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
export class AuthService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
signup(email: string, password: string) {
|
||||||
|
return this.http.post<TokenData>('http://localhost:8800/signup',
|
||||||
|
{
|
||||||
|
username: email,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
).pipe(catchError(errorRes => {
|
||||||
|
let errorMessage = 'An unknown error occurred!';
|
||||||
|
const err = Error(errorMessage);
|
||||||
|
if (!errorRes.error) {
|
||||||
|
return throwError(() => err);
|
||||||
|
}
|
||||||
|
switch(errorRes.statusText) {
|
||||||
|
case 'ERROR':
|
||||||
|
errorMessage = 'Uups...';
|
||||||
|
}
|
||||||
|
return throwError(() => Error(errorMessage));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
login(email: string, password: string): Observable<TokenData> {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('username', email)
|
||||||
|
.append('password', password);
|
||||||
|
return this.http.post<TokenData>('http://127.0.0.1:8800/api/login/token', params,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
button {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-left: 14px;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
@if(error) {
|
||||||
|
<div class="alert alter-danger">
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(isLoading) {
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<app-loading-spinner></app-loading-spinner>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@else {
|
||||||
|
<form (ngSubmit)="onSubmit(authForm)" #authForm="ngForm">
|
||||||
|
<div class="form-group" style="margin-bottom: 7px;">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input type="email" id="email" class="form-control" ngModel name="email" required email=""/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 7px;">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" class="form-control" ngModel name="password" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!authForm.valid">
|
||||||
|
{{ isLoginMode ? 'Login' : 'Sign Up' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" (click)="onSwitchMode()" type="button">
|
||||||
|
Switch to {{ isLoginMode ? 'Sign Up' : 'Login' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Auth } from './auth';
|
||||||
|
|
||||||
|
describe('Auth', () => {
|
||||||
|
let component: Auth;
|
||||||
|
let fixture: ComponentFixture<Auth>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Auth]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Auth);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NgForm } from '@angular/forms';
|
||||||
|
import { AuthService, TokenData } from './auth-service';
|
||||||
|
import { LoadingSpinner } from "../../shared/loading-spinner/loading-spinner";
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-auth',
|
||||||
|
imports: [FormsModule, LoadingSpinner],
|
||||||
|
templateUrl: './auth.html',
|
||||||
|
styleUrl: './auth.css'
|
||||||
|
})
|
||||||
|
export class Auth {
|
||||||
|
isLoginMode = true;
|
||||||
|
isLoading = false;
|
||||||
|
error: string | null = null;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
onSwitchMode() {
|
||||||
|
this.isLoginMode =!this.isLoginMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(form: NgForm) {
|
||||||
|
if (!form.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const email = form.value.email;
|
||||||
|
const password = form.value.password;
|
||||||
|
|
||||||
|
let authObservable: Observable<TokenData>;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
if (this.isLoginMode) {
|
||||||
|
authObservable = this.authService.login(email, password);
|
||||||
|
} else {
|
||||||
|
authObservable = this.authService.signup(email, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
authObservable.subscribe({
|
||||||
|
next: token => {
|
||||||
|
console.log(token);
|
||||||
|
localStorage.setItem('userToken', JSON.stringify(token));
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
console.log(err);
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
div {
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div>
|
||||||
|
<a [routerLink]="['/', 'comic', 'artist', artist().id]" routerLinkActive="active">
|
||||||
|
<span>{{ artist().name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicArtistComponent } from './comic-artist.component';
|
||||||
|
|
||||||
|
describe('ComicArtistComponent', () => {
|
||||||
|
let component: ComicArtistComponent;
|
||||||
|
let fixture: ComponentFixture<ComicArtistComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicArtistComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicArtistComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { Artist } from '../comic.model';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-artist',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './comic-artist.component.html',
|
||||||
|
styleUrl: './comic-artist.component.css'
|
||||||
|
})
|
||||||
|
export class ComicArtistComponent {
|
||||||
|
artist = input.required<Artist>();
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
ul {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<ul>
|
||||||
|
@for (artist of artists(); track artist.id) {
|
||||||
|
<li>
|
||||||
|
<app-comic-artist [artist]="artist"/>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicArtistsListComponent } from './comic-artists-list.component';
|
||||||
|
|
||||||
|
describe('ComicArtistsListComponent', () => {
|
||||||
|
let component: ComicArtistsListComponent;
|
||||||
|
let fixture: ComponentFixture<ComicArtistsListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicArtistsListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicArtistsListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
|
||||||
|
import { Artist } from '../comic.model';
|
||||||
|
import { ComicArtistComponent } from '../comic-artist/comic-artist.component';
|
||||||
|
import { ArtistService } from '../comic-artists/artist.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-artists-list',
|
||||||
|
imports: [ComicArtistComponent],
|
||||||
|
templateUrl: './comic-artists-list.component.html',
|
||||||
|
styleUrl: './comic-artists-list.component.css'
|
||||||
|
})
|
||||||
|
export class ComicArtistsListComponent implements OnInit {
|
||||||
|
|
||||||
|
artists = signal<Artist[]>([]);
|
||||||
|
isFetching = signal(false);
|
||||||
|
error = signal('');
|
||||||
|
private artistService = inject(ArtistService);
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isFetching.set(true);
|
||||||
|
const subscription = this.artistService.loadArtists().subscribe({
|
||||||
|
next: (artists) => {
|
||||||
|
this.artists.set(artists);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
this.error.set(error.message);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.isFetching.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.destroyRef.onDestroy(() => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { inject, Injectable, signal } from "@angular/core";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { catchError, map, throwError } from "rxjs";
|
||||||
|
import { ErrorService } from "../../../shared/error.service";
|
||||||
|
import { Artist, ArtistDetails } from "../comic.model";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ArtistService {
|
||||||
|
private errorService = inject(ErrorService);
|
||||||
|
private httpClient = inject(HttpClient);
|
||||||
|
private artists = signal<Artist[]>([]);
|
||||||
|
|
||||||
|
loadedArtists = this.artists.asReadonly();
|
||||||
|
|
||||||
|
loadArtists() {
|
||||||
|
return this.fetchArtists('http://127.0.0.1:8800/api/comics/artists', 'Someting went wrong fetching artists. Please try again later-');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadArtistDetails(artistId: string | null) {
|
||||||
|
return this.fetchArtistDetails('http://127.0.0.1:8800/api/comics/artists/' + artistId, 'Someting went wrong fetching comic artists. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchArtists(url: string, errorMessage: string) {
|
||||||
|
return this.httpClient.get<Artist[]>(url).pipe(
|
||||||
|
map((resData) => resData),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchArtistDetails(url: string, errorMessage: string) {
|
||||||
|
return this.httpClient.get<ArtistDetails>(url).pipe(
|
||||||
|
map((resData) => {
|
||||||
|
console.log(resData);
|
||||||
|
return resData;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 20px;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: darkgrey;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="grid-container">
|
||||||
|
<div>
|
||||||
|
<app-comic-artists-list />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (artist()) {
|
||||||
|
<section>
|
||||||
|
<h2>{{ artist().name }}</h2>
|
||||||
|
<a href="{{ artist().weblink }}" style="background-color: green;">{{ artist().name }}</a>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
@for (work of artist().comic_works; track work.worktype.id) {
|
||||||
|
<app-comic-worktype [worktype]="work.worktype"/>
|
||||||
|
@for (comic of work.comics; track comic.id) {
|
||||||
|
<article>
|
||||||
|
<app-comic-comic [comic]="comic"/>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
@for (work of artist().issue_works; track work.worktype.id) {
|
||||||
|
<app-comic-worktype [worktype]="work.worktype"/>
|
||||||
|
@for (issue of work.issues; track issue.id) {
|
||||||
|
<article>
|
||||||
|
<app-comic-comic [comic]="issue.comic"/>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
} @else {
|
||||||
|
<h2>Artist Details</h2>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicArtistsComponent } from './comic-artists.component';
|
||||||
|
|
||||||
|
describe('ComicArtistsComponent', () => {
|
||||||
|
let component: ComicArtistsComponent;
|
||||||
|
let fixture: ComponentFixture<ComicArtistsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicArtistsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicArtistsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component, inject, input } from '@angular/core';
|
||||||
|
import { ComicArtistsListComponent } from '../comic-artists-list/comic-artists-list.component';
|
||||||
|
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { ArtistService } from './artist.service';
|
||||||
|
import { ComicComicComponent } from "../comic-comic/comic-comic.component";
|
||||||
|
import { ComicWorktypeComponent } from "../comic-worktype/comic-worktype.component";
|
||||||
|
import { ArtistDetails } from '../comic.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-artists',
|
||||||
|
imports: [ComicArtistsListComponent, ComicComicComponent, ComicWorktypeComponent],
|
||||||
|
templateUrl: './comic-artists.component.html',
|
||||||
|
styleUrl: './comic-artists.component.css'
|
||||||
|
})
|
||||||
|
export class ComicArtistsComponent {
|
||||||
|
artist = input.required<ArtistDetails>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const artistResolver: ResolveFn<ArtistDetails> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
|
const artistService = inject(ArtistService);
|
||||||
|
const artistId = route.paramMap.get('artistId');
|
||||||
|
const artistDetails = artistService.loadArtistDetails(artistId);
|
||||||
|
console.log(artistDetails);
|
||||||
|
return artistDetails;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div>
|
||||||
|
<a [routerLink]="['/', 'comic', 'comics', comic().id]" routerLinkActive="active">
|
||||||
|
<span>{{ comic().title }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicComicComponent } from './comic-comic.component';
|
||||||
|
|
||||||
|
describe('ComicComicComponent', () => {
|
||||||
|
let component: ComicComicComponent;
|
||||||
|
let fixture: ComponentFixture<ComicComicComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicComicComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicComicComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { Comic } from '../comic.model';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-comic',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './comic-comic.component.html',
|
||||||
|
styleUrl: './comic-comic.component.css'
|
||||||
|
})
|
||||||
|
export class ComicComicComponent {
|
||||||
|
comic = input.required<Comic>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
ul {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<input #searchQuery type="text" (input)="search($event.target.value)" placeholder="Search" />
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
@for (comic of comics(); track comic.id) {
|
||||||
|
<li>
|
||||||
|
<app-comic-comic [comic]="comic"/>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicComicsListComponent } from './comic-comics-list.component';
|
||||||
|
|
||||||
|
describe('ComicComicsListComponent', () => {
|
||||||
|
let component: ComicComicsListComponent;
|
||||||
|
let fixture: ComponentFixture<ComicComicsListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicComicsListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicComicsListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core';
|
||||||
|
import { Comic } from '../comic.model';
|
||||||
|
import { ComicService } from '../comic-comics/comic.service';
|
||||||
|
import { ComicComicComponent } from '../comic-comic/comic-comic.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-comics-list',
|
||||||
|
imports: [ComicComicComponent],
|
||||||
|
templateUrl: './comic-comics-list.component.html',
|
||||||
|
styleUrl: './comic-comics-list.component.css',
|
||||||
|
})
|
||||||
|
export class ComicComicsListComponent implements OnInit {
|
||||||
|
comicsFetched = signal<Comic[] | undefined>(undefined);
|
||||||
|
searchQuery = signal<string>('');
|
||||||
|
comics = computed(() => {
|
||||||
|
const sq = this.searchQuery();
|
||||||
|
return this.comicsFetched()?.filter(c => c.title.toLowerCase().includes(sq.toLowerCase()));
|
||||||
|
});
|
||||||
|
isFetching = signal(false);
|
||||||
|
error = signal('');
|
||||||
|
private comicsService = inject(ComicService);
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isFetching.set(true);
|
||||||
|
const subscription = this.comicsService.loadComics().subscribe({
|
||||||
|
next: (comics) => {
|
||||||
|
this.comicsFetched.set(comics);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
this.error.set(error.message);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.isFetching.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.destroyRef.onDestroy(() => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query: string) {
|
||||||
|
this.searchQuery.set(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.float-parent-element {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
.float-child-element {
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
border-width: 10px;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
.float-details-element {
|
||||||
|
border: 1px solid darkgreen;
|
||||||
|
background-color: lightgreen;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent {
|
||||||
|
border: 1px solid black;
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 2rem 2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.child {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid red;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="grid-container">
|
||||||
|
<div>
|
||||||
|
<app-comic-comics-list/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (comic()) {
|
||||||
|
<section>
|
||||||
|
<h2>{{ comic().title }}</h2>
|
||||||
|
<a href="{{ comic().weblink }}" style="background-color: green;">{{ comic().title }}</a>
|
||||||
|
<app-comic-publisher [publisher]="comic().publisher" />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
@for (issue of comic().issues; track issue.id) {
|
||||||
|
<app-comic-issue [issue]="issue"/>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
@for (work of comic().works; track work.worktype.id) {
|
||||||
|
<app-comic-worktype [worktype]="work.worktype"/>
|
||||||
|
@for (artist of work.artists; track artist.id) {
|
||||||
|
<article>
|
||||||
|
<app-comic-artist [artist]="artist"/>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
} @else {
|
||||||
|
<h2>Comic Details</h2>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicComicsComponent } from './comic-comics.component';
|
||||||
|
|
||||||
|
describe('ComicComicsComponent', () => {
|
||||||
|
let component: ComicComicsComponent;
|
||||||
|
let fixture: ComponentFixture<ComicComicsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicComicsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicComicsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component, inject, input } from '@angular/core';
|
||||||
|
import { ComicComicsListComponent } from "../comic-comics-list/comic-comics-list.component";
|
||||||
|
import { ComicDetails } from '../comic.model';
|
||||||
|
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { ComicService } from './comic.service';
|
||||||
|
import { ComicWorktypeComponent } from '../comic-worktype/comic-worktype.component';
|
||||||
|
import { ComicArtistComponent } from '../comic-artist/comic-artist.component';
|
||||||
|
import { ComicIssueComponent } from '../comic-issue/comic-issue.component';
|
||||||
|
import { ComicPublisherComponent } from "../comic-publisher/comic-publisher.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-comics',
|
||||||
|
imports: [ComicComicsListComponent, ComicWorktypeComponent, ComicArtistComponent, ComicIssueComponent, ComicPublisherComponent],
|
||||||
|
templateUrl: './comic-comics.component.html',
|
||||||
|
styleUrl: './comic-comics.component.css'
|
||||||
|
})
|
||||||
|
export class ComicComicsComponent {
|
||||||
|
comic = input.required<ComicDetails>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const comicResolver: ResolveFn<ComicDetails> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
|
const comicService = inject(ComicService);
|
||||||
|
const comicId = route.paramMap.get('comicId');
|
||||||
|
const comicDetails = comicService.loadComicDetails(comicId);
|
||||||
|
console.log(comicDetails);
|
||||||
|
return comicDetails;
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { inject, Injectable, signal } from "@angular/core";
|
||||||
|
import { Comic, ComicDetails } from "../comic.model";
|
||||||
|
import { catchError, map, throwError } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ComicService {
|
||||||
|
private httpClient = inject(HttpClient);
|
||||||
|
private comics = signal<Comic[]>([]);
|
||||||
|
|
||||||
|
loadedComics = this.comics.asReadonly();
|
||||||
|
|
||||||
|
loadComics() {
|
||||||
|
return this.fetchComics('http://127.0.0.1:8800/api/comics/comics', 'Someting went wrong fetching comics. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadComicDetails(comicId: string | null) {
|
||||||
|
return this.fetchComicDetails('http://127.0.0.1:8800/api/comics/comics/' + comicId, 'Someting went wrong fetching comics. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchComicDetails(url: string, errorMessage: string) {
|
||||||
|
return this.httpClient.get<ComicDetails>(url).pipe(
|
||||||
|
map((resData) => {
|
||||||
|
console.log(resData);
|
||||||
|
return resData;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchComics(url: string, errorMessage: string) {
|
||||||
|
return this.httpClient.get<Comic[]>(url).pipe(
|
||||||
|
map((resData) => resData),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div>
|
||||||
|
<a [routerLink]="['/', 'comic', 'issue', issue().id]" routerLinkActive="active">
|
||||||
|
<span>{{ issue().issue_number }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicIssueComponent } from './comic-issue.component';
|
||||||
|
|
||||||
|
describe('ComicIssueComponent', () => {
|
||||||
|
let component: ComicIssueComponent;
|
||||||
|
let fixture: ComponentFixture<ComicIssueComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicIssueComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicIssueComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { Issue } from '../comic.model';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-issue',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './comic-issue.component.html',
|
||||||
|
styleUrl: './comic-issue.component.css'
|
||||||
|
})
|
||||||
|
export class ComicIssueComponent {
|
||||||
|
issue = input.required<Issue>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div class="subnav">
|
||||||
|
<a routerLink="/comic/comics" routerLinkActive="active">Comics</a>
|
||||||
|
<a routerLink="/comic/publisher" routerLinkActive="active">Publisher</a>
|
||||||
|
<a routerLink="/comic/artist" routerLinkActive="active">Artists</a>
|
||||||
|
</div>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicNavigationComponent } from './comic-navigation.component';
|
||||||
|
|
||||||
|
describe('ComicNavigationComponent', () => {
|
||||||
|
let component: ComicNavigationComponent;
|
||||||
|
let fixture: ComponentFixture<ComicNavigationComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicNavigationComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicNavigationComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-navigation',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './comic-navigation.component.html',
|
||||||
|
styleUrl: './comic-navigation.component.css'
|
||||||
|
})
|
||||||
|
export class ComicNavigationComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div>
|
||||||
|
<a [routerLink]="['/', 'comic', 'publisher', publisher().id]" routerLinkActive="active">
|
||||||
|
<span>{{ publisher().name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicPublisherComponent } from './comic-publisher.component';
|
||||||
|
|
||||||
|
describe('ComicPublisherComponent', () => {
|
||||||
|
let component: ComicPublisherComponent;
|
||||||
|
let fixture: ComponentFixture<ComicPublisherComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicPublisherComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicPublisherComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { Publisher } from '../comic.model';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-publisher',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './comic-publisher.component.html',
|
||||||
|
styleUrl: './comic-publisher.component.css'
|
||||||
|
})
|
||||||
|
export class ComicPublisherComponent {
|
||||||
|
publisher = input.required<Publisher>();
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
ul {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<ul>
|
||||||
|
@for (publisher of publishers(); track publisher.id) {
|
||||||
|
<li>
|
||||||
|
<app-comic-publisher [publisher]="publisher" />
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicPublishersListComponent } from './comic-publishers-list.component';
|
||||||
|
|
||||||
|
describe('ComicPublishersListComponent', () => {
|
||||||
|
let component: ComicPublishersListComponent;
|
||||||
|
let fixture: ComponentFixture<ComicPublishersListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicPublishersListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicPublishersListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
|
||||||
|
import { Publisher } from '../comic.model';
|
||||||
|
import { PublisherService } from '../comic-publishers/publisher.service';
|
||||||
|
import { ComicPublisherComponent } from "../comic-publisher/comic-publisher.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-publishers-list',
|
||||||
|
imports: [ComicPublisherComponent],
|
||||||
|
templateUrl: './comic-publishers-list.component.html',
|
||||||
|
styleUrl: './comic-publishers-list.component.css'
|
||||||
|
})
|
||||||
|
export class ComicPublishersListComponent implements OnInit {
|
||||||
|
publishers = signal<Publisher[]>([]);
|
||||||
|
isFetching = signal(false);
|
||||||
|
error = signal('');
|
||||||
|
private publisherService = inject(PublisherService);
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isFetching.set(true);
|
||||||
|
const subscription = this.publisherService.loadPublishers().subscribe({
|
||||||
|
next: (publishers) => {
|
||||||
|
this.publishers.set(publishers);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
this.error.set(error.message);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.isFetching.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.destroyRef.onDestroy(() => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<div class="grid-container">
|
||||||
|
<div>
|
||||||
|
<app-comic-publishers-list />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (publisher()) {
|
||||||
|
<section>
|
||||||
|
<h2>{{ publisher().name }}</h2>
|
||||||
|
@if (publisher().parent_publisher) {
|
||||||
|
<app-comic-publisher [publisher]="publisher().parent_publisher" />
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
@for (imprint of publisher().imprints; track imprint.id) {
|
||||||
|
<article>
|
||||||
|
<app-comic-publisher [publisher]="imprint" />
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
@for (comic of publisher().comics; track comic.id) {
|
||||||
|
<article>
|
||||||
|
<app-comic-comic [comic]="comic" />
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
} @else {
|
||||||
|
<h2>Publisher Details</h2>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicPublishersComponent } from './comic-publishers.component';
|
||||||
|
|
||||||
|
describe('ComicPublishersComponent', () => {
|
||||||
|
let component: ComicPublishersComponent;
|
||||||
|
let fixture: ComponentFixture<ComicPublishersComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicPublishersComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicPublishersComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component, inject, input } from '@angular/core';
|
||||||
|
import { Publisher, PublisherDetails } from '../comic.model';
|
||||||
|
import { ComicPublishersListComponent } from '../comic-publishers-list/comic-publishers-list.component';
|
||||||
|
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { PublisherService } from './publisher.service';
|
||||||
|
import { ComicPublisherComponent } from "../comic-publisher/comic-publisher.component";
|
||||||
|
import { ComicComicComponent } from "../comic-comic/comic-comic.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-publishers',
|
||||||
|
imports: [ComicPublishersListComponent, ComicPublisherComponent, ComicComicComponent],
|
||||||
|
templateUrl: './comic-publishers.component.html',
|
||||||
|
styleUrl: './comic-publishers.component.css'
|
||||||
|
})
|
||||||
|
export class ComicPublishersComponent {
|
||||||
|
publisher = input.required<PublisherDetails>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publisherResolver: ResolveFn<PublisherDetails> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
|
const publisherService = inject(PublisherService);
|
||||||
|
const publisherId = route.paramMap.get('publisherId');
|
||||||
|
const publisherDetails = publisherService.loadPublisherDetails(publisherId);
|
||||||
|
console.log(publisherDetails);
|
||||||
|
return publisherDetails;
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { inject, Injectable, signal } from "@angular/core";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { catchError, map, throwError } from "rxjs";
|
||||||
|
import { ErrorService } from "../../../shared/error.service";
|
||||||
|
import { Publisher, PublisherDetails } from "../comic.model";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PublisherService {
|
||||||
|
private errorService = inject(ErrorService);
|
||||||
|
private httpClient = inject(HttpClient);
|
||||||
|
private publishers = signal<Publisher[]>([]);
|
||||||
|
|
||||||
|
loadedPublishers = this.publishers.asReadonly();
|
||||||
|
|
||||||
|
loadPublishers() {
|
||||||
|
return this.fetchPublishers('http://127.0.0.1:8800/api/comics/publishers', 'Someting went wrong fetching artists. Please try again later-');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPublisherDetails(artistId: string | null) {
|
||||||
|
return this.fetchPublisherDetails('http://127.0.0.1:8800/api/comics/publishers/' + artistId, 'Someting went wrong fetching comic artists. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchPublishers(url: string, errorMessage: string) {
|
||||||
|
return this.httpClient.get<Publisher[]>(url).pipe(
|
||||||
|
map((resData) => resData),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchPublisherDetails(url: string, errorMessage: string) {
|
||||||
|
return this.httpClient.get<PublisherDetails>(url).pipe(
|
||||||
|
map((resData) => {
|
||||||
|
console.log(resData);
|
||||||
|
return resData;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return throwError(() => new Error(errorMessage));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<app-comic-navigation />
|
||||||
|
<router-outlet></router-outlet>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicSectionComponent } from './comic-section.component';
|
||||||
|
|
||||||
|
describe('ComicSectionComponent', () => {
|
||||||
|
let component: ComicSectionComponent;
|
||||||
|
let fixture: ComponentFixture<ComicSectionComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicSectionComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ComicNavigationComponent } from "../comic-navigation/comic-navigation.component";
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-section',
|
||||||
|
imports: [ComicNavigationComponent, RouterOutlet],
|
||||||
|
templateUrl: './comic-section.component.html',
|
||||||
|
styleUrl: './comic-section.component.css'
|
||||||
|
})
|
||||||
|
export class ComicSectionComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Routes } from "@angular/router";
|
||||||
|
import { artistResolver, ComicArtistsComponent } from "../comic-artists/comic-artists.component";
|
||||||
|
import { ComicPublishersComponent, publisherResolver } from './../comic-publishers/comic-publishers.component';
|
||||||
|
import { ComicComicsComponent, comicResolver } from "../comic-comics/comic-comics.component";
|
||||||
|
|
||||||
|
export const comicRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'comics',
|
||||||
|
component: ComicComicsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'comics/:comicId',
|
||||||
|
component: ComicComicsComponent,
|
||||||
|
resolve: {
|
||||||
|
comic: comicResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'publisher',
|
||||||
|
component: ComicPublishersComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'publisher/:publisherId',
|
||||||
|
component: ComicPublishersComponent,
|
||||||
|
resolve: {
|
||||||
|
publisher: publisherResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'artist',
|
||||||
|
component: ComicArtistsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'artist/:artistId',
|
||||||
|
component: ComicArtistsComponent,
|
||||||
|
resolve: {
|
||||||
|
artist: artistResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div>
|
||||||
|
<a [routerLink]="['/', 'comic', 'worktype', worktype().id]" routerLinkActive="active">
|
||||||
|
<span>{{ worktype().name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ComicWorktypeComponent } from './comic-worktype.component';
|
||||||
|
|
||||||
|
describe('ComicWorktypeComponent', () => {
|
||||||
|
let component: ComicWorktypeComponent;
|
||||||
|
let fixture: ComponentFixture<ComicWorktypeComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ComicWorktypeComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ComicWorktypeComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
import { Worktype } from '../comic.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-comic-worktype',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './comic-worktype.component.html',
|
||||||
|
styleUrl: './comic-worktype.component.css'
|
||||||
|
})
|
||||||
|
export class ComicWorktypeComponent {
|
||||||
|
worktype = input.required<Worktype>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
export interface Artist {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Worktype {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Publisher {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Comic {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Volume {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Issue {
|
||||||
|
id: string;
|
||||||
|
issue_number: string;
|
||||||
|
in_stock: boolean;
|
||||||
|
is_read: boolean;
|
||||||
|
comic: Comic;
|
||||||
|
volume: Volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComicWork {
|
||||||
|
worktype: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComicWorktypeArtists {
|
||||||
|
worktype: Worktype;
|
||||||
|
artists: Artist[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublisherDetails {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
parent_publisher: Publisher;
|
||||||
|
imprints: Publisher[];
|
||||||
|
comics: Comic[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComicDetails {
|
||||||
|
id: string;
|
||||||
|
created: string;
|
||||||
|
title: string;
|
||||||
|
completed: boolean;
|
||||||
|
current_order: boolean;
|
||||||
|
weblink: string;
|
||||||
|
publisher: Publisher;
|
||||||
|
issues: Issue[];
|
||||||
|
volumes: Volume[];
|
||||||
|
works: ComicWorktypeArtists[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistWorktypeComics {
|
||||||
|
worktype: Worktype;
|
||||||
|
comics: Comic[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistWorktypeIssues {
|
||||||
|
worktype: Worktype;
|
||||||
|
issues: Issue[];
|
||||||
|
}
|
||||||
|
export interface ArtistDetails {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
weblink: string;
|
||||||
|
comic_works: ArtistWorktypeComics[];
|
||||||
|
issue_works: ArtistWorktypeIssues[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: lightblue;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- <div> -->
|
||||||
|
<footer class="footer">
|
||||||
|
<a href="{{ footerUrl }}">{{ footerLink }}</a>
|
||||||
|
</footer>
|
||||||
|
<!-- </div> -->
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KontorFooterComponent } from './footer.component';
|
||||||
|
|
||||||
|
describe('KontorFooterComponent', () => {
|
||||||
|
let component: KontorFooterComponent;
|
||||||
|
let fixture: ComponentFixture<KontorFooterComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KontorFooterComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KontorFooterComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'kontor-footer',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './footer.component.html',
|
||||||
|
styleUrl: './footer.component.css'
|
||||||
|
})
|
||||||
|
export class KontorFooterComponent {
|
||||||
|
footerUrl = "https://kontor.thpeetz.de";
|
||||||
|
footerLink = "kontor.thpeetz.de";
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* Header/Blog Title */
|
||||||
|
.header {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<header class="header">
|
||||||
|
<section>
|
||||||
|
<h1>{{ title() }}</h1>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KontorHeaderComponent } from './header.component';
|
||||||
|
|
||||||
|
describe('Header', () => {
|
||||||
|
let component: KontorHeaderComponent;
|
||||||
|
let fixture: ComponentFixture<KontorHeaderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KontorHeaderComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KontorHeaderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'kontor-header',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
styleUrl: './header.component.css'
|
||||||
|
})
|
||||||
|
export class KontorHeaderComponent {
|
||||||
|
protected readonly title = signal('Kontor');
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>kontor works!</p>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KontorComponent } from './kontor.component';
|
||||||
|
|
||||||
|
describe('Kontor', () => {
|
||||||
|
let component: KontorComponent;
|
||||||
|
let fixture: ComponentFixture<KontorComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [KontorComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(KontorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-kontor',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './kontor.component.html',
|
||||||
|
styleUrl: './kontor.component.css'
|
||||||
|
})
|
||||||
|
export class KontorComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>media-actors works!</p>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MediaActorsComponent } from './media-actors.component';
|
||||||
|
|
||||||
|
describe('MediaActorsComponent', () => {
|
||||||
|
let component: MediaActorsComponent;
|
||||||
|
let fixture: ComponentFixture<MediaActorsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MediaActorsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MediaActorsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-media-actors',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './media-actors.component.html',
|
||||||
|
styleUrl: './media-actors.component.css'
|
||||||
|
})
|
||||||
|
export class MediaActorsComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<div>
|
||||||
|
<article>
|
||||||
|
<a [routerLink]="['/', 'media', 'file', mediafile().id]" routerLinkActive="active">
|
||||||
|
<span>{{ mediafile().title }}</span>
|
||||||
|
</a>
|
||||||
|
<a href="{{ mediafile().url }}"> >></a>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
@if (mediafile().review) {
|
||||||
|
Review<img class="images" src="tick.png" />
|
||||||
|
} @else {
|
||||||
|
Review<img class="images" src="cross.png" />
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MediaFileComponent } from './media-file.component';
|
||||||
|
|
||||||
|
describe('MediaFileComponent', () => {
|
||||||
|
let component: MediaFileComponent;
|
||||||
|
let fixture: ComponentFixture<MediaFileComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MediaFileComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MediaFileComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { MediaFile } from '../media-files/media-file.model';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-media-file',
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './media-file.component.html',
|
||||||
|
styleUrl: './media-file.component.css'
|
||||||
|
})
|
||||||
|
export class MediaFileComponent {
|
||||||
|
mediafile = input.required<MediaFile>();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user