Compare commits
2 Commits
develop/0.2.0
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eae6e07f4 | |||
| b26b5ecc9c |
+5
-13
@@ -1,16 +1,8 @@
|
|||||||
.idea/
|
|
||||||
.theia/
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.angular/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
bonus/
|
node_modules/
|
||||||
icons/
|
.editorconfig
|
||||||
icons-shadowless/
|
|
||||||
springboot/.factorypath
|
|
||||||
java-ee/.settings
|
|
||||||
java-ee/.project
|
|
||||||
kontor-schema/env
|
|
||||||
kontor-schema/kontor_schema.egg-info
|
|
||||||
kontor-gui/.pdm-python
|
|
||||||
kontor-gui/dist
|
|
||||||
fastapi/.coverage
|
|
||||||
db-password.txt
|
db-password.txt
|
||||||
|
couchdb-password.txt
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
kontor_api := kontor-api
|
kontor_api := kontor-api
|
||||||
kontor_spring := kontor-spring
|
kontor_spring := kontor-spring
|
||||||
|
kontor_angular := kontor-angular
|
||||||
|
TARGET=docker
|
||||||
|
|
||||||
.PHONY: all $(kontor_spring) $(kontor_api)
|
.PHONY: all $(kontor_spring) $(kontor_api) $(kontor_angular)
|
||||||
all: $(kontor_spring) $(kontor_api)
|
|
||||||
|
|
||||||
$(kontor_spring) $(kontor_api):
|
all: $(kontor_spring) $(kontor_api) $(kontor_angular)
|
||||||
|
|
||||||
|
$(kontor_spring) $(kontor_api) $(kontor_angular):
|
||||||
$(MAKE) --directory=$@ $(TARGET)
|
$(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,38 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=kontor
|
||||||
|
- POSTGRES_USER=kontor
|
||||||
|
#- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
|
||||||
|
- POSTGRES_PASSWORD=kontor
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U kontor"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data:rw
|
||||||
|
secrets:
|
||||||
|
- db-password
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
ports:
|
||||||
|
- 8090:8080
|
||||||
|
networks:
|
||||||
|
- database
|
||||||
|
- frontend
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
secrets:
|
||||||
|
db-password:
|
||||||
|
file: db-password.txt
|
||||||
|
networks:
|
||||||
|
database:
|
||||||
|
name: database
|
||||||
|
external: true
|
||||||
+164
-19
@@ -1,44 +1,189 @@
|
|||||||
|
include:
|
||||||
|
- ./compose-postgres.yaml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mariadb:
|
activemq:
|
||||||
image: mariadb
|
image: apache/activemq-artemis:latest-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: kontor
|
|
||||||
MYSQL_USER: kontor
|
|
||||||
MYSQL_PASSWORD: kontor
|
|
||||||
MYSQL_DATABASE: kontor
|
|
||||||
ports:
|
ports:
|
||||||
- 3316:3306
|
- 61616:61616
|
||||||
|
- 8161:8161
|
||||||
|
- 5672:5672
|
||||||
networks:
|
networks:
|
||||||
- database
|
- integration
|
||||||
|
- frontend
|
||||||
volumes:
|
volumes:
|
||||||
- mariadb-storage:/var/lib/mysql:rw
|
- activemq-data:/var/lib/artemis-instance
|
||||||
kontor:
|
kontor:
|
||||||
image: kontor
|
build:
|
||||||
|
context: ./kontor-spring
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- kontor:0.2.0-SNAPSHOT
|
||||||
|
image: kontor:0.2.0-SNAPSHOT
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- database
|
- database
|
||||||
|
- integration
|
||||||
- frontend
|
- frontend
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8100:8100
|
||||||
|
volumes:
|
||||||
|
- images-data:/data/images
|
||||||
depends_on:
|
depends_on:
|
||||||
- mariadb
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
kontor-api:
|
kontor-api:
|
||||||
image: kontor-api
|
build:
|
||||||
|
context: ./kontor-api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
tags:
|
||||||
|
- 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:
|
networks:
|
||||||
- database
|
- 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:
|
||||||
|
- database
|
||||||
|
- integration
|
||||||
- frontend
|
- frontend
|
||||||
ports:
|
ports:
|
||||||
- 8800:8800
|
- 8800:8800
|
||||||
depends_on:
|
depends_on:
|
||||||
- mariadb
|
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:
|
networks:
|
||||||
database:
|
integration:
|
||||||
|
name: integration
|
||||||
frontend:
|
frontend:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mariadb-storage:
|
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