181 Commits

Author SHA1 Message Date
tpeetz 0b3dadc18d integrate kontor-quarkus in docker compose 2026-01-18 23:12:14 +01:00
tpeetz a788381eaa add first comic endpoints to kontor-quarkus 2026-01-18 01:39:52 +01:00
tpeetz a031c5cc99 add kontor-quarkus 2026-01-17 18:10:31 +01:00
Thomas Peetz db1a56965f add deleting MediaActors during import 2026-01-16 12:58:32 +01:00
tpeetz 9677323597 find actor by url or name 2026-01-14 07:13:01 +01:00
tpeetz 56c6808508 update log config for kontor-api
fix double occurances of log output and change level to Debug for
Profile information
2026-01-10 15:39:06 +01:00
Thomas Peetz e5d4b748dc add endpoints for kontor-api 2026-01-09 17:04:46 +01:00
tpeetz 98eb72bd22 add removal of remaining items by calling API 2026-01-08 17:54:02 +01:00
tpeetz 0dbd108051 add authentication for kontor-scripts 2026-01-08 07:05:33 +01:00
tpeetz d63629ba5c add authentication for kontor-scripts 2026-01-07 23:46:54 +01:00
tpeetz 696c3e77be add TYSC schema for koontor-echo and add tests 2026-01-06 23:21:57 +01:00
tpeetz 73f92f6770 remove method Connect in kontor-echo 2026-01-03 13:38:27 +01:00
tpeetz 4d93f51767 rename kontor projects 2026-01-03 04:07:42 +01:00
tpeetz 0392ac49fb add project kontor-api-echo 2026-01-03 02:53:50 +01:00
tpeetz fe919eaa35 implement Comics schema and endpoints 2026-01-01 23:21:41 +01:00
tpeetz a57cd9c294 implement endpots api/v1/comics and api/v1/media/files 2026-01-01 16:32:38 +01:00
tpeetz 0accddaad9 remove SessionFactory and prepare for creating an EntityManagerFactory 2025-12-31 00:11:40 +01:00
tpeetz 15a0c8701c add Hibernate to kontor-javalin 2025-12-30 14:43:48 +01:00
Thomas Peetz d4dbfa58e9 add models for kontor-javalin 2025-12-30 03:08:14 +01:00
tpeetz b87f0fc60a refactor kontor-spring 2025-12-29 17:24:01 +01:00
tpeetz 41733ec030 extends add_links to get title and actors 2025-12-29 02:51:03 +01:00
tpeetz 66cbd4b2d1 Merge branch 'feature/Dockerfile-multistage' into 'develop/0.2.0'
add kontor-javalin and change to multistage Dockerfile for Gradle projects

See merge request tpeetz/kontor!37
2025-12-21 21:44:23 +01:00
tpeetz 11b13e0bf4 add healthcheck for kontor-javalin 2025-12-21 21:15:08 +01:00
tpeetz 3c4e182832 added running kontor-javalin with example API 2025-12-21 20:14:31 +01:00
tpeetz 7ef616a2a9 add task fatJar 2025-12-21 19:42:22 +01:00
tpeetz d3ccc2a46b create logs directory in Dockerfile for kontor-spring 2025-12-21 19:27:49 +01:00
tpeetz e0a235fcc5 change Dockerfile for kontor-spring to multistage and add kontor-javalin 2025-12-20 04:13:41 +01:00
tpeetz 25bc4e45ff add kontor-javalin 2025-12-16 23:10:15 +01:00
tpeetz 2a51b4a2f3 add links from file 2025-12-14 16:12:22 +01:00
tpeetz 729d019de9 format download.py 2025-12-09 17:05:20 +01:00
tpeetz 430a69a95c fix missing line feed 2025-12-09 15:35:50 +01:00
Thomas Peetz 0a53e68b54 add limit for downloads 2025-12-09 13:48:22 +01:00
tpeetz e061e0aadb fix declaration of network database as external 2025-12-07 23:30:32 +01:00
tpeetz 6d78d60ecf fix declaration of network database as external 2025-12-07 22:35:59 +01:00
Thomas Peetz 3a0c062e78 change port for kontor-spring to enable port 800 for paperless-ngx 2025-12-07 21:24:32 +01:00
Thomas Peetz a5393f471f fix login functionality 2025-12-04 17:23:59 +01:00
Thomas Peetz 46bca919d7 add authentication for kontor-api-go 2025-12-01 06:53:03 +01:00
Thomas Peetz 741032ec02 add schema for MediaFile 2025-12-01 05:25:54 +01:00
Thomas Peetz d63120b092 add kontor-api-go REST API with Go, Fiber, Bun 2025-11-30 01:50:00 +01:00
Thomas Peetz 836a10e3ef store token 2025-11-13 19:10:46 +01:00
Thomas Peetz 8ca73b94aa fix call for login endpoint 2025-11-12 23:21:39 +01:00
Thomas Peetz 447030533f setup Auth component 2025-11-12 21:06:28 +01:00
Thomas Peetz 66a93b2b97 setup AuthComponent 2025-11-12 17:29:40 +01:00
Thomas Peetz 6c71086104 add handling of missing urls 2025-11-07 08:00:52 +01:00
Thomas Peetz 4a2048c378 add healthcheck to kontor-api and docker-compose.yml 2025-11-06 16:37:50 +01:00
Thomas Peetz f3e47126b3 add login functions for cookie and non-cookie authentication 2025-11-05 21:48:25 +01:00
Thomas Peetz 09c2a350e4 add JWT dependencies 2025-10-17 22:14:32 +02:00
Thomas Peetz 92cd9be8ec add first draft of search bar to comics list 2025-10-06 19:40:28 +02:00
Thomas Peetz e6ead4937d display imprints and parent publisher of comics publisher 2025-10-03 20:35:06 +02:00
tpeetz 5e17182686 Merge branch 'feature/34-refactor-kontor-angular' into 'develop/0.2.0'
Resolve "refactor kontor-angular"

Closes #34

See merge request tpeetz/kontor!36
2025-10-03 16:06:57 +02:00
Thomas Peetz c52a5b8715 Extend display of comics publishers 2025-10-03 16:04:52 +02:00
Thomas Peetz a9829bf118 extend ComicDetailsResponse 2025-10-02 14:18:48 +02:00
Thomas Peetz d8e4cbfb9a display Comics as Artists 2025-10-01 21:06:12 +02:00
Thomas Peetz 0db55e9ba7 added issue works on artist details 2025-09-23 17:08:46 +02:00
Thomas Peetz 2534c67a5e moved interface to common place 2025-09-22 14:55:38 +02:00
Thomas Peetz 6d88b87f93 add comic artists 2025-09-21 20:09:46 +02:00
Thomas Peetz 64ed4876a5 change response for comic details 2025-09-20 13:05:07 +02:00
Thomas Peetz fd7a6bd1a1 load comic details 2025-09-19 22:34:44 +02:00
Thomas Peetz b250bfe76c WIP: setup styling 2025-09-19 14:50:45 +02:00
Thomas Peetz 63ac0231dc add route for specific comic 2025-09-19 00:55:04 +02:00
Thomas Peetz 21533ee9f9 add links for tysc types 2025-09-19 00:17:04 +02:00
Thomas Peetz 30f9829768 add links for media types 2025-09-18 23:56:53 +02:00
Thomas Peetz a5b1d771a0 add links for comics and publishers 2025-09-18 23:49:39 +02:00
Thomas Peetz b5425c62de add links for artists 2025-09-18 23:40:09 +02:00
Thomas Peetz 0cc2561327 add routes for Media section 2025-09-18 23:07:41 +02:00
Thomas Peetz f933312283 add routes for TYSC section 2025-09-18 22:58:19 +02:00
Thomas Peetz a0d2bef4ef add routes for Comic section 2025-09-18 22:46:23 +02:00
Thomas Peetz eb8b283769 setup header, navigation and footer of app 2025-09-18 22:33:58 +02:00
Thomas Peetz fcfe9a2c08 Revert "move Kontor related components to subfolder kontor"
This reverts commit 9ee1a04a5c.
2025-09-18 22:13:39 +02:00
Thomas Peetz 9ee1a04a5c move Kontor related components to subfolder kontor 2025-09-18 21:59:14 +02:00
Thomas Peetz 933974c958 add Artist Details 2025-09-15 17:08:42 +02:00
Thomas Peetz 5ff9d5a11b fetch list of artists 2025-09-14 10:47:17 +02:00
Thomas Peetz 8b9313da93 add navigation and fetching of sports 2025-09-12 23:51:38 +02:00
Thomas Peetz 9acdff19e1 add routing for comics and media 2025-09-10 17:25:22 +02:00
Thomas Peetz ad1118803d setup kontor-angiöar with comics navigation 2025-09-08 22:10:23 +02:00
Thomas Peetz 55a09d32b3 improve download.py 2025-09-08 13:38:47 +02:00
Thomas Peetz 8b1b84b195 refactor find_links.py by adding methods for specific tasks 2025-09-08 12:42:01 +02:00
Thomas Peetz acbf9c51a3 refactor find_links.py by adding methods for specific tasks 2025-09-07 22:47:06 +02:00
Thomas Peetz 5bfea51b27 improve find_links.py 2025-09-06 19:55:33 +02:00
Thomas Peetz a398866f04 improve find_links.py 2025-09-05 13:01:11 +02:00
Thomas Peetz ee737aaf61 add Actors page to navigation 2025-09-04 18:07:07 +02:00
Thomas Peetz c9460ac198 add actors 2025-09-03 22:00:30 +02:00
Thomas Peetz 3c5f5e50e0 add adding of actors 2025-09-03 17:45:20 +02:00
Thomas Peetz b40b9538ab adapt logging levels for messages 2025-09-02 20:39:21 +02:00
Thomas Peetz 2fa77d3af6 adapt logging levels for messages 2025-08-31 13:50:31 +02:00
Thomas Peetz a6a03e3f04 fix problem in download.py when title has not been set 2025-08-19 08:55:45 +02:00
Thomas Peetz ddc5797549 fix problem in download.py when title has not been set 2025-08-18 13:42:58 +02:00
Thomas Peetz 65f5e4bb2e add styles from kontor-api to kontor-vue 2025-08-18 00:06:12 +02:00
tpeetz 976a116799 Merge branch 'feature/33-use-css-for-header' into 'develop/0.2.0'
Resolve "use CSS for header"

Closes #33

See merge request tpeetz/kontor!35
2025-08-16 00:40:32 +02:00
tpeetz c8736577e5 Resolve "use CSS for header" 2025-08-16 00:40:32 +02:00
Thomas Peetz 60d314abaf split docker-compose.yml to extract database services 2025-08-10 02:55:56 +02:00
Thomas Peetz 0810ca8d50 update versions 2025-08-07 22:03:37 +02:00
tpeetz 68b4bbb979 Merge branch 'feature/31-setup-vue-app' into 'develop/0.2.0'
Resolve "Setup Vue app"

Closes #31

See merge request tpeetz/kontor!33
2025-08-07 21:08:28 +02:00
Thomas Peetz 427850bd64 setup Example Vue app
refs #31
2025-08-07 21:07:40 +02:00
Thomas Peetz 6201e433ae add VUE.js app 2025-08-07 18:33:34 +02:00
Thomas Peetz 450bfdb394 add column to MediaActor to store weblink 2025-08-03 17:37:57 +02:00
tpeetz aed2413b88 Merge branch 'feature/29-setup-angular-app' into 'develop/0.2.0'
Setup Angular app

Closes #29

See merge request tpeetz/kontor!32
2025-07-29 20:57:25 +02:00
Thomas Peetz 01788286e2 setup Angular app and integrate Docker image build in docker-compose.yml 2025-07-20 11:19:05 +02:00
Thomas Peetz 169b349277 add handling of error condition before renaming file 2025-07-18 23:37:00 +02:00
Thomas Peetz 9c0de2a0be set version for kontor-angular to 0.2.0 2025-07-18 21:29:24 +02:00
Thomas Peetz 3b4098cb1a setup Angular app 2025-07-18 17:17:00 +02:00
Thomas Peetz 0d99f383fb add MediaFile from queue 2025-07-11 16:49:42 +02:00
Thomas Peetz c6fd80408b read queue 2025-07-10 22:04:31 +02:00
Thomas Peetz d2d4deb350 set messages durable 2025-07-10 15:58:11 +02:00
tpeetz ee280e0b8e Merge branch 'feature/27-use-amqp-protocol-for-messages' into 'develop/0.2.0'
use AMQP protocol for messages

Closes #27

See merge request tpeetz/kontor!30
2025-07-07 15:52:04 +02:00
Thomas Peetz 1c2c2f38a4 use message for adding links 2025-07-07 15:49:33 +02:00
Thomas Peetz b5cca50960 add_link.py sends message 2025-07-06 22:46:47 +02:00
tpeetz 0a505fabcf Merge branch 'feature/25-integrate-apache-camel' into 'develop/0.2.0'
integrate apache camel

Closes #25 and #26

See merge request tpeetz/kontor!29
2025-07-04 00:29:48 +02:00
Thomas Peetz 4b8053c6e2 fix configuration to connect to Artemis from Sprint Boot for all environments 2025-07-04 00:29:15 +02:00
Thomas Peetz 5df3c1c47e integrate Apache Camel into Spring Boot 2025-07-03 17:42:30 +02:00
Thomas Peetz adad4f88da configure actuator endpoints 2025-07-03 11:31:40 +02:00
Thomas Peetz 957c7a702e Extend docker-compose.yml network configuration for camel-karavan 2025-07-03 07:33:24 +02:00
tpeetz b5344a9ed1 Merge branch 'feature/24-add-docker-image-for-apache-camel-caravan' into 'develop/0.2.0'
Add Docker image for Apache Camel Caravan

Closes #24

See merge request tpeetz/kontor!28
2025-07-01 22:28:37 +02:00
Thomas Peetz 16ad701eed Extend docker-compose.yml for camel-karavan, registry and gitea 2025-07-01 22:23:24 +02:00
Thomas Peetz 2ba7465675 change port and credentials for messages 2025-07-01 07:01:34 +02:00
tpeetz c703dfc6e3 Merge branch 'feature/23-add-docker-image-for-apache-activemq' into 'develop/0.2.0'
Add Docker image for Apache ActiveMQ

Closes #23

See merge request tpeetz/kontor!26
2025-06-30 17:39:10 +02:00
Thomas Peetz b62cc89f71 add Apache ActiveMQ Artemis as Docker image to docker-compose.yml 2025-06-30 17:33:39 +02:00
tpeetz e741a46f69 Merge branch 'feature/22-add-docker-image-for-couchdb' into 'develop/0.2.0'
Add Docker image for CouchDB

Closes #22

See merge request tpeetz/kontor!25
2025-06-30 16:42:08 +02:00
Thomas Peetz 4f87ff83ce add CouchDB as Docker image to docker-compose.yml 2025-06-30 16:38:15 +02:00
Thomas Peetz 4871f56320 use new SearchField for Comics 2025-06-23 22:17:45 +02:00
Thomas Peetz ec404c9956 remove servicemix 2025-06-23 12:48:07 +02:00
tpeetz fc9db8200f Merge branch 'feature/6-enhance-the-search-field' into 'develop/0.2.0'
Enhance the search field by adding options to filter for boolean fields

See merge request tpeetz/kontor!22
2025-06-23 12:39:03 +02:00
tpeetz 69caa825de Enhance the search field by adding options to filter for boolean fields 2025-06-23 12:39:03 +02:00
Thomas Peetz 123be2e9c0 add script to read queues 2025-06-20 16:57:17 +02:00
Thomas Peetz 928536b414 add servicemix container build 2025-06-17 23:35:35 +02:00
Thomas Peetz 8ed8599f17 add missing import 2025-06-16 08:08:30 +02:00
Thomas Peetz 435de2ec92 fix handling of id 2025-06-15 05:31:42 +02:00
Thomas Peetz 529a199262 add add_link.py and improve hamndling of MediaFile 2025-06-14 18:53:15 +02:00
Thomas Peetz 510e2f8130 make buttons submit and cancel work 2025-06-13 01:11:18 +02:00
Thomas Peetz da4f286180 remove unused imports 2025-06-11 18:56:33 +02:00
Thomas Peetz d9136e45f6 WIP: add HTML form for editing comics 2025-06-09 23:38:55 +02:00
Thomas Peetz b610947403 change loading config for export.py 2025-06-08 20:23:30 +02:00
Thomas Peetz 6e520a46f0 add display of issue title in lists 2025-06-07 21:39:01 +02:00
Thomas Peetz b4a0c2d7a5 make details for Comic, Artist and Issue clickable, add CustomField to select Comic and Issue 2025-06-05 17:58:27 +02:00
Thomas Peetz ea9f596abe add IssueWork entity with Repository, Form and View 2025-06-04 21:36:26 +02:00
Thomas Peetz 4bb7d61f80 add comparison of datetime objects with strings 2025-06-04 14:06:18 +02:00
Thomas Peetz cc8a166f5c change log.info to log.debug 2025-06-03 13:38:49 +02:00
Thomas Peetz a70bf8ae96 fix selecting Volume for Issue 2025-06-03 10:18:36 +02:00
tpeetz 902ee03e3f Merge branch 'feature/19-create-component-for-selecting-year-and-month' into 'develop/0.2.0'
Resolve "Create component for selecting year and month"

Closes #19

See merge request tpeetz/kontor!19
2025-06-02 22:31:26 +02:00
Thomas Peetz f1f49ab014 use CustomField to create combined year and month field 2025-06-02 22:30:28 +02:00
Thomas Peetz 7a16225c3b add custom component MonthYearPicker 2025-06-02 16:39:03 +02:00
Thomas Peetz 8e13af5b8c add handling of None values 2025-06-02 08:53:40 +02:00
tpeetz 767c069404 Merge branch 'feature/18-remove-entity-moduledata' into 'develop/0.2.0'
Remove entity ModuleData and import data for comic, admin and tysc module

Closes #18

See merge request tpeetz/kontor!20
2025-06-01 22:29:04 +02:00
Thomas Peetz dd5281e2a1 remove entity ModuleData from python code
refs #18
2025-06-01 22:27:42 +02:00
Thomas Peetz aefd56d1ff Remove entity ModuleData and setup data
refs #18
2025-06-01 22:23:25 +02:00
tpeetz 293a0b3478 remove dependency to MariaDB and improve import of field published_on 2025-06-01 20:23:15 +02:00
tpeetz 076466b895 remove dependency to MariaDB 2025-06-01 19:56:01 +02:00
tpeetz 370738ff14 Merge branch 'feature/16-add-publication-date' into 'develop/0.2.0'
Add field published_on and title to Issue

See merge request tpeetz/kontor!18
2025-06-01 18:37:19 +02:00
Thomas Peetz 1151b0e45e Add field published_on and title to Issue
/refs #16

Add field published_on and title to Issue and display both fields.
2025-06-01 18:32:15 +02:00
Thomas Peetz 7c5c571716 add form for editing artist 2025-05-27 18:08:17 +02:00
Thomas Peetz 1b636efdb7 change logging 2025-05-26 11:35:51 +02:00
Thomas Peetz 374c242890 deactivate authentication 2025-05-25 23:10:05 +02:00
Thomas Peetz 87b1c24783 move card components to module specific directories 2025-05-25 22:12:23 +02:00
Thomas Peetz 456162da44 remove MetaDataTable and MetaDataColumn to cleanup schema 2025-05-23 18:21:41 +02:00
Thomas Peetz 8cfb60f9a1 add missing fields for import 2025-05-21 23:24:13 +02:00
Thomas Peetz 4c70046a32 removed fields from MetaDataColumn 2025-05-21 17:29:14 +02:00
Thomas Peetz 546f8ebbdf separate modules for worktype endpoints 2025-05-20 14:06:13 +02:00
Thomas Peetz bd86379d07 add missing fields for comic 2025-05-19 15:14:29 +02:00
Thomas Peetz edcaed1b1a fix problem when adding columns 2025-05-19 14:34:26 +02:00
Thomas Peetz 20ed0b2f40 add method for import data 2025-05-19 14:16:30 +02:00
Thomas Peetz 3197288eee add update for existing items 2025-05-18 15:44:13 +02:00
Thomas Peetz 84d64f04d2 check existing items with data from file 2025-05-15 13:46:05 +02:00
tpeetz 1ecf64a228 update model for import 2025-05-14 19:36:15 +02:00
Thomas Peetz 41d513e402 add reference from Volume to Issue and Comic 2025-05-13 21:40:29 +02:00
Thomas Peetz aa4b47e032 change display of Comics from cards to table 2025-05-13 07:57:58 +02:00
Thomas Peetz 3537642df9 add CRUD for WorkType 2025-05-13 00:42:41 +02:00
Thomas Peetz 06a48a03ac add display for WorkType 2025-05-09 01:17:35 +02:00
Thomas Peetz d60e606663 add display for MediaVideo 2025-05-05 22:13:51 +02:00
Thomas Peetz 13dad3961c add display for MetaData 2025-05-04 12:27:03 +02:00
Thomas Peetz 4cf1941f44 add issue display 2025-05-02 18:17:06 +02:00
Thomas Peetz ace568a800 setup changed 2025-05-02 13:58:09 +02:00
Thomas Peetz c77adb0e04 update models to use string type for id fields 2025-05-02 11:21:57 +02:00
Thomas Peetz 7ff2bf912d update scripts to use Postgres 2025-05-01 01:13:49 +02:00
Thomas Peetz 72c1a7d265 use PostgreSQL for kontor-spring and kontor-api 2025-04-30 22:11:48 +02:00
Thomas Peetz 2efacf6d67 Merge branch 'main' into develop/0.2.0 2025-04-30 19:32:42 +02:00
Thomas Peetz d6549132ea update version to 0.2.0 2025-04-30 19:24:29 +02:00
Thomas Peetz 58f80b3e76 create release 0.1.0 2025-04-30 18:40:24 +02:00
tpeetz ce49c463c3 Merge branch 'develop/0.1.0' into 'main'
remove obsolete kontor.py

See merge request tpeetz/kontor!17
2025-04-30 17:31:18 +02:00
tpeetz 931b4a0aba remove obsolete kontor.py 2025-04-30 17:31:18 +02:00
Thomas Peetz bbf422cc5d create Docker images only with development version as tag 2025-04-29 14:43:07 +02:00
Thomas Peetz 4c96de27db import sources from develop/0.1.0 2025-04-29 12:52:55 +02:00
571 changed files with 35728 additions and 5022 deletions
+5 -13
View File
@@ -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
+6 -3
View File
@@ -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)
+22
View File
@@ -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
+38
View File
@@ -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
View File
@@ -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:
+3
View File
@@ -0,0 +1,3 @@
dist
node_modules
+42
View File
@@ -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
+14
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
.PHONY: docker dev
docker:
docker build -t kontor-angular:0.2.0-SNAPSHOT .
dev:
ng serve
+59
View File
@@ -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.
+95
View File
@@ -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
}
}
+37
View File
@@ -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;
}
}
+9970
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -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

+9
View File
@@ -0,0 +1,9 @@
h1 {
color: blue;
}
.app {
font-family: Arial, Helvetica, sans-serif;
/* max-width: 500px; */
margin: auto;
}
+10
View File
@@ -0,0 +1,10 @@
<div class="app">
<kontor-header />
<app-navigation/>
<main>
<router-outlet />
</main>
<kontor-footer />
</div>
+14
View File
@@ -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 {
}
+13
View File
@@ -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(),
]
};
+26
View File
@@ -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,
},
];
+23
View File
@@ -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>();
}
@@ -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;
}
}
@@ -0,0 +1,7 @@
<ul>
@for (artist of artists(); track artist.id) {
<li>
<app-comic-artist [artist]="artist"/>
</li>
}
</ul>
@@ -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();
});
});
@@ -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;
}
}
@@ -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>
@@ -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>
@@ -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>();
}
@@ -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;
}
}
@@ -0,0 +1,7 @@
<ul>
@for (publisher of publishers(); track publisher.id) {
<li>
<app-comic-publisher [publisher]="publisher" />
</li>
}
</ul>
@@ -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();
});
});
@@ -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>
@@ -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