Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c918f01db | |||
| 709ac9512c | |||
| 450d4986cc | |||
| 781f6dd854 | |||
| b81a38e650 | |||
| 92ddf5e95f | |||
| dd1503963e | |||
| 0951cc74ff | |||
| b458f3b6d4 | |||
| 8857d60058 | |||
| cc9d54f671 | |||
| 69fb3c1e97 | |||
| 03751452e9 | |||
| 3ec44effe1 | |||
| 3cf46d65a8 | |||
| 97e0cf8a7a | |||
| 2713594224 | |||
| aa1f73961f | |||
| b32e8fee3d | |||
| 1338c56ee1 | |||
| 587539e8a0 | |||
| f525f5d8c4 | |||
| cf770f4814 | |||
| 25fa07d517 | |||
| bb701a903d | |||
| aff1720972 | |||
| b1127130cd | |||
| eab4aaf037 | |||
| 7eae6e07f4 | |||
| b26b5ecc9c | |||
| 58f80b3e76 | |||
| ce49c463c3 | |||
| 931b4a0aba |
@@ -0,0 +1,8 @@
|
|||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.angular/
|
||||||
|
__pycache__/
|
||||||
|
node_modules/
|
||||||
|
.editorconfig
|
||||||
|
db-password.txt
|
||||||
|
couchdb-password.txt
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Kontor
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
kontor_api := kontor-api
|
||||||
|
kontor_spring := kontor-spring
|
||||||
|
kontor_angular := kontor-angular
|
||||||
|
TARGET=docker
|
||||||
|
|
||||||
|
.PHONY: all $(kontor_spring) $(kontor_api) $(kontor_angular)
|
||||||
|
|
||||||
|
all: $(kontor_spring) $(kontor_api) $(kontor_angular)
|
||||||
|
|
||||||
|
$(kontor_spring) $(kontor_api) $(kontor_angular):
|
||||||
|
$(MAKE) --directory=$@ $(TARGET)
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
couchdb:
|
||||||
|
image: couchdb
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- COUCHDB_USER=admin
|
||||||
|
- COUCHDB_PASSWORD=admin
|
||||||
|
ports:
|
||||||
|
- 5984:5984
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- frontend
|
||||||
|
volumes:
|
||||||
|
- couchdb-data:/opt/couchdb/data
|
||||||
|
secrets:
|
||||||
|
- couchdb-password
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
couchdb-data:
|
||||||
|
secrets:
|
||||||
|
couchdb-password:
|
||||||
|
file: couchdb-password.txt
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
|
||||||
|
services:
|
||||||
|
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:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8800:8800
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-angular:
|
||||||
|
build:
|
||||||
|
context: ./kontor-angular
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-angular:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-angular:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8200:80
|
||||||
|
depends_on:
|
||||||
|
kontor-api:
|
||||||
|
condition: service_healthy
|
||||||
|
kontor-vue:
|
||||||
|
build:
|
||||||
|
context: ./kontor-vue
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor-vue:0.2.0-SNAPSHOT
|
||||||
|
image: kontor-vue:0.2.0-SNAPSHOT
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
|
- frontend
|
||||||
|
ports:
|
||||||
|
- 8300:80
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
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:
|
||||||
|
integration:
|
||||||
|
name: integration
|
||||||
|
frontend:
|
||||||
|
volumes:
|
||||||
|
activemq-data:
|
||||||
|
images-data:
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
### STAGE 1: Build ###
|
||||||
|
FROM node:22.15-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
### STAGE 2: Run ###
|
||||||
|
FROM nginx:1.17.1-alpine
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.PHONY: docker dev
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build -t kontor-angular:0.2.0-SNAPSHOT .
|
||||||
|
|
||||||
|
dev:
|
||||||
|
ng serve
|
||||||
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# KontorAngular
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.2.1.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"kontor-angular": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "kontor-angular:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "kontor-angular:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular/build:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
server {
|
||||||
|
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
# Definieren der Standard-Indexdatei (Angular erstellt die Datei index.html für uns und sie befindet sich im oben genannten Verzeichnis)
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Cache-Header für Medien-ASsets
|
||||||
|
location ~* \.(?:cur|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ {
|
||||||
|
access_log off;
|
||||||
|
add_header Pragma "must-revalidate, public";
|
||||||
|
add_header Cache-Control "must-revalidate, public";
|
||||||
|
expires max;
|
||||||
|
|
||||||
|
tcp_nodelay off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache-Header für HTML, CSS und JS-Dateien
|
||||||
|
location ~* \.(?:css|js|html)$ {
|
||||||
|
access_log off;
|
||||||
|
add_header Pragma "must-revalidate, public";
|
||||||
|
add_header Cache-Control "must-revalidate, public";
|
||||||
|
expires 2d;
|
||||||
|
|
||||||
|
tcp_nodelay off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Konfiguration für den /-Pfad
|
||||||
|
location / {
|
||||||
|
# Zunächst versuchen wir die angeforderte URI auzuliefern
|
||||||
|
# Klappt das nicht, versuchen wir es mit einem abschließenden Slash
|
||||||
|
# Klappt auch das nicht, liefern wir die index.html aus.
|
||||||
|
# Das ist nötig, damit Angular-Routen korrekt augeflöst und ausgeliefert werden
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Generated
+9970
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "kontor-angular",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^20.2.0",
|
||||||
|
"@angular/compiler": "^20.2.0",
|
||||||
|
"@angular/core": "^20.2.0",
|
||||||
|
"@angular/forms": "^20.2.0",
|
||||||
|
"@angular/platform-browser": "^20.2.0",
|
||||||
|
"@angular/router": "^20.2.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^20.2.1",
|
||||||
|
"@angular/cli": "^20.2.1",
|
||||||
|
"@angular/compiler-cli": "^20.2.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.9.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
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 {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
|
provideRouter(routes, withComponentInputBinding()),
|
||||||
|
provideHttpClient(),
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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 = [
|
||||||
|
{ 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,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { App } from './app';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [App],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, 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 {
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user