95 Commits

Author SHA1 Message Date
Thomas Peetz 14a4461f6c remove obsolete subprojects
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2026-06-01 20:12:23 +02:00
Thomas Peetz 81ad30bb45 setup Quarkus app und retrieve Publisher 2026-06-01 20:12:23 +02:00
tpeetz 3893a31e42 Enhance the search field by adding options to filter for boolean fields 2026-06-01 20:12:23 +02:00
Thomas Peetz b04f1aa451 add script to read queues 2026-06-01 20:12:23 +02:00
Thomas Peetz 26f8b992af add servicemix container build 2026-06-01 20:12:23 +02:00
Thomas Peetz 23a48d2472 add add_link.py and improve hamndling of MediaFile 2026-06-01 20:12:23 +02:00
Thomas Peetz 56de399c07 WIP: add HTML form for editing comics 2026-06-01 20:12:23 +02:00
Thomas Peetz 7bc211c9bd add display of issue title in lists 2026-06-01 20:12:23 +02:00
Thomas Peetz 4a6afcfb76 make details for Comic, Artist and Issue clickable, add CustomField to select Comic and Issue 2026-06-01 20:12:23 +02:00
Thomas Peetz 26a91274c2 add IssueWork entity with Repository, Form and View 2026-06-01 20:12:23 +02:00
Thomas Peetz 518a72c6e8 use CustomField to create combined year and month field 2026-06-01 20:12:20 +02:00
Thomas Peetz 02d177fedb add custom component MonthYearPicker 2026-06-01 20:12:20 +02:00
Thomas Peetz 16ee2541dc Remove entity ModuleData and setup data
refs #18
2026-06-01 20:12:20 +02:00
tpeetz 3c5ba33922 remove dependency to MariaDB and improve import of field published_on 2026-06-01 20:12:20 +02:00
tpeetz 1a227d41d7 remove dependency to MariaDB 2026-06-01 20:12:20 +02:00
Thomas Peetz 2d07199bce Add field published_on and title to Issue
/refs #16

Add field published_on and title to Issue and display both fields.
2026-06-01 20:12:08 +02:00
Thomas Peetz b457847178 deactivate authentication 2026-06-01 20:12:08 +02:00
Thomas Peetz 89caedc0d2 move card components to module specific directories 2026-06-01 20:12:08 +02:00
Thomas Peetz e0396eb5a9 remove MetaDataTable and MetaDataColumn to cleanup schema 2026-06-01 20:12:08 +02:00
Thomas Peetz c4705a2d5e add missing fields for import 2026-06-01 20:12:02 +02:00
Thomas Peetz 142c515780 removed fields from MetaDataColumn 2026-06-01 20:12:02 +02:00
Thomas Peetz fab7bd145e add missing fields for comic 2026-06-01 20:12:02 +02:00
Thomas Peetz 230e94aa70 add method for import data 2026-06-01 20:12:02 +02:00
tpeetz fc2a78cd2c update model for import 2026-06-01 20:11:56 +02:00
Thomas Peetz e1fca67157 add reference from Volume to Issue and Comic 2026-06-01 20:11:56 +02:00
Thomas Peetz 57ae257e0c change display of Comics from cards to table 2026-06-01 20:11:56 +02:00
Thomas Peetz 18ef6efc4e add CRUD for WorkType 2026-06-01 20:11:56 +02:00
Thomas Peetz 17f507479a add display for WorkType 2026-06-01 20:11:53 +02:00
Thomas Peetz 6a89ab8d0f add display for MediaVideo 2026-06-01 20:11:53 +02:00
Thomas Peetz 30f9e89c60 add display for MetaData 2026-06-01 20:11:53 +02:00
Thomas Peetz aa375573de add issue display 2026-06-01 20:11:53 +02:00
Thomas Peetz cf49408d0c setup changed 2026-06-01 20:11:53 +02:00
Thomas Peetz 1786fecfed update models to use string type for id fields 2026-06-01 20:11:53 +02:00
Thomas Peetz e5fa32d653 update scripts to use Postgres 2026-06-01 20:11:49 +02:00
Thomas Peetz 595786d126 use PostgreSQL for kontor-spring and kontor-api 2026-06-01 20:11:49 +02:00
Thomas Peetz 19a5623075 import sources from develop/0.1.0 2026-06-01 20:11:49 +02:00
tpeetz c30bdd1cd5 add checking dirs to cjeck_kontor.py 2026-06-01 20:11:49 +02:00
tpeetz 2d706cc3d8 fix problem when deleting MediaFile with MediaActor relations 2026-06-01 20:11:49 +02:00
tpeetz 9cb71f18c2 check for duplicate links 2026-06-01 20:11:49 +02:00
Thomas Peetz 061be5a3b4 add missing endpoints for creating items 2026-06-01 20:11:49 +02:00
tpeetz 330d414e72 synchronize data between configured servers 2026-06-01 20:11:43 +02:00
Thomas Peetz f9d09d5357 update cli scripts to use REST API 2026-06-01 20:11:36 +02:00
Thomas Peetz d862476097 complete loading single items 2026-06-01 20:11:32 +02:00
tpeetz 87c6fb7071 check if more than one server is configured before syncing 2026-06-01 20:11:11 +02:00
Thomas Peetz 3ba1fa40fd add endpoints to get items by id 2026-06-01 20:11:10 +02:00
Thomas Peetz faa1ec44c9 added all missing endpoints 2026-06-01 20:11:06 +02:00
tpeetz 7cdbadbf84 add missing comics endpoints 2026-06-01 20:11:03 +02:00
tpeetz 6310377d84 Remove obsolete endpoints (#89)
Remove endpoints api/login/token and api/login/profile

---------

Co-authored-by: Thomas Peetz <thomas.peetz@cimt-ag.de>
Reviewed-on: #89
2026-06-01 20:11:03 +02:00
tpeetz 7b374b1eeb add mailaccount endpoint 2026-06-01 20:10:43 +02:00
Thomas Peetz fb289ab298 add missing endpoints 2026-06-01 20:10:38 +02:00
tpeetz b2cf1c4698 secure media endpoints 2026-06-01 20:10:21 +02:00
tpeetz f1abf03b35 add missing endpoints 2026-06-01 20:10:14 +02:00
tpeetz eeb2f23e71 remove obsolete scripts 2026-06-01 20:10:04 +02:00
Thomas Peetz ab997d7bbd fix typo in workflow demo.yaml 2026-06-01 20:10:04 +02:00
Thomas Peetz fc89ee2b86 test gitea actions 2026-06-01 20:10:04 +02:00
Thomas Peetz 5ad5a84b09 Add environment variable for DB server 2026-06-01 20:10:04 +02:00
Thomas Peetz 72cafe08ac move *.service files to subdirectory container and add service file for standalone operations in standalone 2026-06-01 20:10:04 +02:00
Thomas Peetz 8cbd04c97a renamed systemd unit files 2026-06-01 20:10:04 +02:00
tpeetz 188f405b23 use separate files for chapters 2026-06-01 20:10:04 +02:00
tpeetz 746fd48de7 add subprojekt kontor-doc 2026-06-01 20:10:04 +02:00
tpeetz fa5f4cfa36 Einrichtung Build 2026-06-01 20:10:04 +02:00
tpeetz b5726af898 integrate kontor-angular 2026-06-01 20:10:04 +02:00
tpeetz b59ecc6ac9 integration von kontor-angular 2026-06-01 20:09:58 +02:00
tpeetz 9dce493c2a rename Dockerfile to Containerfile and integrate kontor-angular in pod kontor 2026-06-01 20:09:58 +02:00
Thomas Peetz 72f6591819 update usage of api for login by email or username 2026-06-01 20:09:48 +02:00
Thomas Peetz 2b7b483019 improve konto-vue/Containerfile by explicitly stating images 2026-06-01 20:09:41 +02:00
tpeetz c8fdb032ea fix OAuth authentication 2026-06-01 20:09:41 +02:00
tpeetz 32971afd94 update systemd service files and add script to copy these files 2026-06-01 20:09:32 +02:00
tpeetz 123c7ef6ef update backend url for profiles 2026-06-01 20:09:26 +02:00
tpeetz 313b07e875 update kontor-vue 2026-06-01 20:09:26 +02:00
tpeetz bba417922c re-setup subproject kontor-vue 2026-06-01 20:09:26 +02:00
tpeetz ea39bbbe1d add Vue 3 subproject 2026-06-01 20:09:22 +02:00
tpeetz 0715e7fdea rename Dockerfile to Containerfile and integrate kontor-vue in pod kontor 2026-06-01 20:09:22 +02:00
tpeetz 17de883bce move build tasks to script/cibuild 2026-06-01 20:09:22 +02:00
tpeetz c9a350bc58 change update_title to add authorization to REST calls 2026-06-01 20:09:22 +02:00
Thomas Peetz 9ffbe2cc2f change MediaActor in all applications ffrom unique name to unique url 2026-06-01 20:09:14 +02:00
Thomas Peetz 9e7d31d903 add temporary access when database is empty 2026-06-01 20:09:11 +02:00
Thomas Peetz c77079c9d9 change reference for Java images 2026-06-01 20:09:06 +02:00
tpeetz d6e2b7daa7 remove MongoDB and Mongo-Express 2026-06-01 20:09:01 +02:00
tpeetz 662bc75b0d add MonfoDB and Mongo-Express to pod 2026-06-01 20:09:01 +02:00
tpeetz 6944b3a209 add kontor-javalin to podman pod 2026-06-01 20:09:01 +02:00
tpeetz 99d3756da7 remove obsolete kontor-gui 2026-06-01 20:09:01 +02:00
tpeetz 182d77354e Add kontor-echo and kontor-fiber to kontor-pod
resolve #35

(cherry picked from commit 05e26e512da9c237adbb58510df2b490837cd836)
2026-06-01 20:09:01 +02:00
tpeetz dc577a9b55 add building of images when images not available
(cherry picked from commit eb7f725994c99bece3185869204aac206d6578b2)
2026-06-01 20:09:01 +02:00
tpeetz 13693aad8b use fully describes images names in container builds with Gradle
(cherry picked from commit 57edf92e30cd3e5bec594399b8a21fc1da31d549)
2026-06-01 20:09:01 +02:00
Thomas Peetz 840ca57428 fixed missing mapping in Comic and type in Rooster
(cherry picked from commit 519c359f94b7e901724c851cc55900448f3f74f4)
2026-06-01 20:08:58 +02:00
Thomas Peetz e0b585f72b refactor kontor-api to use SQLAlchemy 2.0 features for mapping fields
(cherry picked from commit e57abdbef7e13e3880738cd639225df5db0c37be)
2026-06-01 20:08:58 +02:00
tpeetz 47df61af32 update kontor-api
(cherry picked from commit f42735326b4dd490351cebb0fc751d62b3a187d0)
2026-06-01 20:08:36 +02:00
Thomas Peetz b2ce2172a2 Umstellung auf podman in Arbeit
(cherry picked from commit 1a92c63ef6d60e9dcba513ebf60cbd9f18a142e8)
2026-06-01 20:08:26 +02:00
tpeetz 65c4dea710 Add postgres and adminer as container to pod kontor
refs #37

(cherry picked from commit 4469010a74ffefe2d92ffa335d7d1225d12fdece)
2026-06-01 20:08:19 +02:00
tpeetz 5659d116c5 Setup scripts to bootstrap, build and update the aplication
refs #36

The scripts are created and will be extended when adding subprojects.

(cherry picked from commit f688fb939c0f754ab70a4c0a014f00e42ff2f9eb)
2026-06-01 20:08:19 +02:00
tpeetz a196f25526 remove obsolete subprojects
(cherry picked from commit e219932b17bf2b9776141828b3212ed210ffa251)
2026-06-01 20:08:19 +02:00
tpeetz 44fac3f471 Vorbereitung Release 0.2.0 2026-06-01 20:08:13 +02:00
Thomas Peetz 729842a71c remove obsolete subprojects 2025-06-28 22:51:01 +02:00
Thomas Peetz 3d1eeeab53 setup Quarkus app und retrieve Publisher 2025-06-28 22:18:45 +02:00
638 changed files with 28211 additions and 14949 deletions
+19
View File
@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} ist testing out Gitea Actions
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: inky
steps:
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "The workflow is now ready to test your code on the runner."
- name: List files in the directory
run: |
ls ${{ gitea.workspace }}
- run: echo "The job's status is ${{ job.status }}."
+15 -3
View File
@@ -2,7 +2,19 @@
.idea/ .idea/
.angular/ .angular/
__pycache__/ __pycache__/
node_modules/ bonus/
.editorconfig icons/
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
kontor-api/.coverage
db-password.txt db-password.txt
couchdb-password.txt kontor-api/tests/test_main.py
kontor-api/tests/test_db.db
kontor-api/test_db.db
+4 -6
View File
@@ -1,12 +1,10 @@
kontor_api := kontor-api kontor_api := kontor-api
kontor_spring := kontor-spring kontor_spring := kontor-spring
kontor_angular := kontor-angular kontor_servicemix := kontor-servicemix
TARGET=docker
.PHONY: all $(kontor_spring) $(kontor_api) $(kontor_angular) .PHONY: all $(kontor_spring) $(kontor_api)
all: $(kontor_spring) $(kontor_api) $(kontor_servicemix)
all: $(kontor_spring) $(kontor_api) $(kontor_angular) $(kontor_spring) $(kontor_api) $(kontor_servicemix):
$(kontor_spring) $(kontor_api) $(kontor_angular):
$(MAKE) --directory=$@ $(TARGET) $(MAKE) --directory=$@ $(TARGET)
-35
View File
@@ -1,35 +0,0 @@
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
+100 -55
View File
@@ -1,25 +1,35 @@
include:
- ./compose-postgres.yaml
services: services:
activemq: postgres:
image: apache/activemq-artemis:latest-alpine image: postgres
restart: unless-stopped 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: ports:
- 61616:61616 - 5432:5432
- 8161:8161
- 5672:5672
networks: networks:
- integration - integration
- frontend - frontend
volumes: volumes:
- activemq-data:/var/lib/artemis-instance - postgres-data:/var/lib/postgresql/data:rw
secrets:
- db-password
adminer:
image: adminer
ports:
- 8090:8080
networks:
- database
- frontend
kontor: kontor:
build:
context: ./kontor-spring
dockerfile: Dockerfile
tags:
- kontor:0.2.0-SNAPSHOT
image: kontor:0.2.0-SNAPSHOT image: kontor:0.2.0-SNAPSHOT
restart: unless-stopped restart: unless-stopped
networks: networks:
@@ -34,13 +44,81 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
kontor-api: kontor-api:
build:
context: ./kontor-api
dockerfile: Dockerfile
tags:
- kontor-api:0.2.0-SNAPSHOT
image: kontor-api:0.2.0-SNAPSHOT image: kontor-api:0.2.0-SNAPSHOT
restart: unless-stopped restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-api:8500/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8500:8500
volumes:
- images-data:/data/images
depends_on:
postgres:
condition: service_healthy
kontor-fiber:
build:
context: ./kontor-fiber
dockerfile: Dockerfile
tags:
- kontor-fiber:0.2.0-SNAPSHOT
image: kontor-fiber:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-fiber:8600/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8600:8600
depends_on:
postgres:
condition: service_healthy
kontor-echo:
build:
context: ./kontor-echo
dockerfile: Dockerfile
tags:
- kontor-echo:0.2.0-SNAPSHOT
image: kontor-echo:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-echo:8700/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- database
- integration
- frontend
ports:
- 8700:8700
depends_on:
postgres:
condition: service_healthy
kontor-quarkus:
build:
context: ./kontor-quarkus
dockerfile: Dockerfile
tags:
- kontor-quarkus:0.2.0-SNAPSHOT
image: kontor-quarkus:0.2.0-SNAPSHOT
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://kontor-quarkus:8800/q/health"]
interval: 10s
timeout: 5s
retries: 3
networks: networks:
- database - database
- integration - integration
@@ -52,47 +130,14 @@ services:
depends_on: depends_on:
postgres: postgres:
condition: service_healthy 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:
postgres:
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
networks: networks:
database:
integration: integration:
name: integration name: integration
frontend: frontend:
volumes: volumes:
activemq-data: postgres-data:
images-data: images-data:
secrets:
db-password:
file: db-password.txt
-9
View File
@@ -1,9 +0,0 @@
build/
.settings/
.gradle/
.classpath
.project
.vscode/
bin/
.idea/
/.eclipse-pmd
-38
View File
@@ -1,38 +0,0 @@
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
before_script:
- source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh"
- sdk u java 11.0.12-open
- chmod +x ./gradlew
stages:
- build
- test
- analysis
- publish
Build Application:
stage: build
script: ./gradlew --build-cache compileJava
Create Documentation:
stage: build
script: ./gradlew asciidoctorPdf
Test Application:
stage: test
script: ./gradlew check
sonarqube-check:
stage: analysis
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
script: ./gradlew sonarqube
allow_failure: true
Publish Artifacts:
stage: publish
script: ./gradlew publish
-158
View File
@@ -1,158 +0,0 @@
plugins {
id 'java'
id 'maven-publish'
id 'jacoco'
id 'jacoco-report-aggregation'
id 'pmd'
id 'checkstyle'
id 'jvm-test-suite'
id 'io.quarkus'
id "org.sonarqube" version "3.3"
alias(versionsLibs.plugins.asciidoctorConvert)
alias(versionsLibs.plugins.asciidoctorPdf)
alias(versionsLibs.plugins.asciidoctorGems)
}
repositories {
ruby.gems()
maven {
url "https://nexus.thpeetz.de/nexus/content/groups/public/"
}
mavenCentral()
mavenLocal()
}
dependencies {
implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
implementation 'io.quarkus:quarkus-resteasy-reactive'
implementation 'io.quarkus:quarkus-resteasy-reactive-jackson'
implementation 'io.quarkus:quarkus-arc'
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'
testImplementation("io.quarkus:quarkus-jacoco")
}
final BUILD_DATE = new Date().format('dd.MM.yyyy').toString()
def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/" + project.name + ".pdf")
def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) {
type 'pdf'
builtBy asciidoctorPdf
}
tasks.withType(GenerateModuleMetadata).configureEach {
suppressedValidationErrors.add('enforced-platform')
}
publishing {
publications {
quarkusApp(MavenPublication){
from components.java
}
docs(MavenPublication) {
groupId = group + '.docs'
artifactId = project.name
artifact pdfArtifact
}
}
repositories {
maven {
name = 'nexusPeetz'
def releasesRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/releases/"
def snapshotsRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/snapshots/"
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
credentials(PasswordCredentials)
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType(GenerateModuleMetadata).configureEach {
suppressedValidationErrors.add('enforced-platform')
}
compileJava {
options.encoding = 'UTF-8'
options.compilerArgs << '-parameters'
}
compileTestJava {
options.encoding = 'UTF-8'
}
jacocoTestReport {
reports {
xml.required = true
}
}
test.finalizedBy jacocoTestReport
pmd {
//ruleSetFiles = files("custom-pmd-ruleset.xml")
ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"]
ignoreFailures = true
}
tasks.withType(Checkstyle) {
reports {
xml.required = true
html.required = true
}
}
testing {
suites {
test {
useJUnitJupiter()
}
}
}
pmdTest.dependsOn("compileQuarkusTestGeneratedSourcesJava")
checkstyleTest.dependsOn("compileQuarkusTestGeneratedSourcesJava")
jacocoTestReport.dependsOn("compileQuarkusGeneratedSourcesJava")
pmdMain.dependsOn("compileQuarkusGeneratedSourcesJava")
sonarqube {
properties {
property "sonar.host.url", "https://sonar.thpeetz.de"
property "sonar.qualitygate.wait", true
property "sonar.sourceEncoding", "UTF-8"
property "sonar.projectKey", "kontor_kontor-quarkus_AYQek8Mxz0hBjLSV8I8O"
property "sonar.login", "5ecd90dee57806857e07443a9b0efd3cd7774a81"
}
}
tasks.named('sonarqube').configure {
dependsOn 'test', 'pmdMain', 'checkstyleMain', 'jacocoTestReport'
}
asciidoctorPdf {
baseDirFollowsSourceFile()
asciidoctorj {
attributes 'build-gradle': file('build.gradle'),
'endpoint-url': 'https://www.thpeetz.de',
'source-highlighter': 'rouge',
'imagesdir': './images',
'toc': 'left',
'toc-title': 'Inhaltsverzeichnis',
'revdate': BUILD_DATE,
'revnumber': { project.version.toString() },
'revremark': 'Entwurf',
'chapter-label': '',
'icons': 'font',
'idprefix': 'id_',
'idseparator': '-',
'docinfo1': ''
}
}
wrapper {
gradleVersion = "7.5"
}
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="UnusedImports" />
<module name="AvoidStarImport" />
<module name="ConstantName" />
<module name="NeedBraces" />
<module name="RightCurly" />
<module name="ModifierOrder">
<property name="severity" value="warning"/>
</module>
</module>
</module>
-9
View File
@@ -1,9 +0,0 @@
#Gradle properties
group=de.thpeetz.kontor
version=1.0.0-SNAPSHOT
quarkusPluginId=io.quarkus
quarkusPluginVersion=2.13.2.Final
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=2.13.2.Final
Binary file not shown.
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
@@ -1,84 +0,0 @@
= Projektbeschreibung kontor-quarkus: Pflichtenheft und Projekthandbuch
:author: Thomas Peetz
:email: <thomas.peetz@thpeetz.de>
:doctype: article
:sectnums:
:sectnumlevels: 4
:toc:
:toclevels: 4
:table-caption!:
:counter: table-number: 0
//[title="Dokumenthistorie", caption="Tabelle {counter:table-number} ", id="Tabelle-{counter:table-number}", options="header"]
//[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header", cols="4"]
[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"]
|===
| Version | Datum | Autor | Änderungsgrund / Bemerkungen
| 0.0.1 | 16.05.2022 | Thomas Peetz | Ersterstellung
|===
== Einführung
=== Zweck
=== Stakeholder des Systems
=== Systemumfang
==== Zielsetzung des Systems
=== Systemübersicht
==== Systemkontext
==== Systemarchitektur
==== Systemschnittstellen
===== Realisierte Schnittstellen
===== Verwendete Schnittstellen
==== Logisches Datenmodell
==== Einschränkungen
== Anforderungen der Domäne
=== Systemfunktionen
==== Anwendungsfälle
==== Akteure
==== Zielgruppen
=== Anforderungen
==== Anforderungen an externe Schnittstellen
==== Funktionale Anforderungen
==== Qualitätsanforderungen
==== Randbedingungen
==== Weitere Anforderungen
==== Wartungs- und Supportinformationen
=== Verifikation
[bibliography]
== Referenzen
[glossary]
== Glossar
== Verzeichnisse
=== Abbildungsverzeichnis
=== Tabellenverzeichnis
<<Table-1, Tabelle 1>> <<Table-1>>
@@ -1,95 +0,0 @@
package de.thpeetz.kontor.comics;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import de.thpeetz.kontor.comics.service.ArtistService;
import de.thpeetz.kontor.comics.service.ComicsService;
import de.thpeetz.kontor.comics.service.PublisherService;
@Path("/kontor/comics")
public class ComicsResource {
@Inject
transient ComicsService comicsService;
@Inject
transient ArtistService artistService;
@Inject
transient PublisherService publisherService;
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/")
public String showComicList() {
return comicsService.showComicList();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/artist")
public String showArtistList() {
return artistService.showArtistList();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/artist")
public String showArtistListJson() {
return artistService.showArtistList();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/artist/view/{artist_id}")
public String showArtist(String artist_id) {
return artistService.showArtist(artist_id);
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/artist/create")
public String showArtistCreation() {
return artistService.showArtistCreation();
}
@POST
@Produces(MediaType.TEXT_HTML)
@Path("/artist/create")
public String validateArtistDetails() {
return artistService.validateArtistDetails();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/publisher")
public String showPublisherList() {
return publisherService.showPublisherList();
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/publisher/view/{publisher_id}")
public String showPublisher(String publisher_id) {
return publisherService.showPublisher(publisher_id);
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/publisher/create")
public String showPublisherCreation() {
return publisherService.showPublisherCreation();
}
@POST
@Produces(MediaType.TEXT_HTML)
@Path("/publisher/create")
public String validatePublisherDetails() {
return publisherService.validatePublisherDetails();
}
}
@@ -1,28 +0,0 @@
package de.thpeetz.kontor.comics.service;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ArtistService {
public String showArtistList() {
// TODO Auto-generated method stub
return null;
}
public String showArtist(String artist_id) {
// TODO Auto-generated method stub
return null;
}
public String showArtistCreation() {
// TODO Auto-generated method stub
return null;
}
public String validateArtistDetails() {
// TODO Auto-generated method stub
return null;
}
}
@@ -1,12 +0,0 @@
package de.thpeetz.kontor.comics.service;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ComicsService {
public String showComicList() {
return "comics";
}
}
@@ -1,28 +0,0 @@
package de.thpeetz.kontor.comics.service;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PublisherService {
public String showPublisherList() {
// TODO Auto-generated method stub
return null;
}
public String showPublisher(String publisher_id) {
// TODO Auto-generated method stub
return null;
}
public String showPublisherCreation() {
// TODO Auto-generated method stub
return null;
}
public String validatePublisherDetails() {
// TODO Auto-generated method stub
return null;
}
}
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
# Your configuration properties
quarkus.http.test-port=8083
quarkus.http.test-ssl-port=8446
@@ -1,8 +0,0 @@
package de.thpeetz.kontor.comics;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class ComicsResourceIT extends ComicsResourceTest {
}
@@ -1,19 +0,0 @@
package de.thpeetz.kontor.comics;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
class ComicsResourceTest {
@Test
void testComicsEndpoint() {
given().when().get("/kontor/comics")
.then()
.statusCode(200)
.body(is("comics"));
}
}
@@ -1,5 +1,5 @@
### STAGE 1: Build ### ### STAGE 1: Build ###
FROM node:22.15-alpine AS build FROM docker.io/node:22-alpine AS build
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm install RUN npm install
@@ -7,8 +7,9 @@ COPY . .
RUN npm run build RUN npm run build
### STAGE 2: Run ### ### STAGE 2: Run ###
FROM nginx:1.17.1-alpine FROM docker.io/library/nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html COPY --from=build /app/dist/kontor-angular/browser /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+1 -1
View File
@@ -1,6 +1,6 @@
# KontorAngular # KontorAngular
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.1.1. This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.2.1.
## Development server ## Development server
+10 -9
View File
@@ -5,11 +5,7 @@
"projects": { "projects": {
"kontor-angular": { "kontor-angular": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {},
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "app", "prefix": "app",
@@ -18,8 +14,10 @@
"builder": "@angular/build:application", "builder": "@angular/build:application",
"options": { "options": {
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [ "assets": [
{ {
"glob": "**/*", "glob": "**/*",
@@ -27,7 +25,7 @@
} }
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.css"
] ]
}, },
"configurations": { "configurations": {
@@ -72,8 +70,11 @@
"test": { "test": {
"builder": "@angular/build:karma", "builder": "@angular/build:karma",
"options": { "options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [ "assets": [
{ {
"glob": "**/*", "glob": "**/*",
@@ -81,7 +82,7 @@
} }
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.css"
] ]
} }
} }
+1
View File
@@ -1,4 +1,5 @@
server { server {
listen 8800;
# Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher) # Root-Verzeichnis für den Server setzen (wir kopieren unsere Anwendung hierher)
root /usr/share/nginx/html; root /usr/share/nginx/html;
+1418 -970
View File
File diff suppressed because it is too large Load Diff
+17 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "kontor-angular", "name": "kontor-angular",
"version": "0.2.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
@@ -9,6 +9,8 @@
"test": "ng test" "test": "ng test"
}, },
"prettier": { "prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [ "overrides": [
{ {
"files": "*.html", "files": "*.html",
@@ -20,26 +22,27 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/common": "^20.1.0", "@angular/common": "^20.2.0",
"@angular/compiler": "^20.1.0", "@angular/compiler": "^20.2.0",
"@angular/core": "^20.1.0", "@angular/core": "^20.2.0",
"@angular/forms": "^20.1.0", "@angular/forms": "^20.2.0",
"@angular/platform-browser": "^20.1.0", "@angular/platform-browser": "^20.2.0",
"@angular/router": "^20.1.0", "@angular/router": "^20.2.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0" "tslib": "^2.3.0",
"zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^20.1.1", "@angular/build": "^20.2.1",
"@angular/cli": "^20.1.1", "@angular/cli": "^20.2.1",
"@angular/compiler-cli": "^20.1.0", "@angular/compiler-cli": "^20.2.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"jasmine-core": "~5.8.0", "jasmine-core": "~5.9.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.8.2" "typescript": "~5.9.2"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

+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 {
}
+6 -5
View File
@@ -1,12 +1,13 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes) provideRouter(routes, withComponentInputBinding()),
provideHttpClient(),
] ]
}; };
-342
View File
@@ -1,342 +0,0 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
--bright-blue: oklch(51.01% 0.274 263.83);
--electric-violet: oklch(53.18% 0.28 296.97);
--french-violet: oklch(47.66% 0.246 305.88);
--vivid-pink: oklch(69.02% 0.277 332.77);
--hot-red: oklch(61.42% 0.238 15.34);
--orange-red: oklch(63.32% 0.24 31.68);
--gray-900: oklch(19.37% 0.006 300.98);
--gray-700: oklch(36.98% 0.014 302.71);
--gray-400: oklch(70.9% 0.015 304.04);
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
180deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
90deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--pill-accent: var(--bright-blue);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
font-size: 3.125rem;
color: var(--gray-900);
font-weight: 500;
line-height: 100%;
letter-spacing: -0.125rem;
margin: 0;
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
}
p {
margin: 0;
color: var(--gray-700);
}
main {
width: 100%;
min-height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
box-sizing: inherit;
position: relative;
}
.angular-logo {
max-width: 9.2rem;
}
.content {
display: flex;
justify-content: space-around;
width: 100%;
max-width: 700px;
margin-bottom: 3rem;
}
.content h1 {
margin-top: 1.75rem;
}
.content p {
margin-top: 1.5rem;
}
.divider {
width: 1px;
background: var(--red-to-pink-to-purple-vertical-gradient);
margin-inline: 0.5rem;
}
.pill-group {
display: flex;
flex-direction: column;
align-items: start;
flex-wrap: wrap;
gap: 1.25rem;
}
.pill {
display: flex;
align-items: center;
--pill-accent: var(--bright-blue);
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
color: var(--pill-accent);
padding-inline: 0.75rem;
padding-block: 0.375rem;
border-radius: 2.75rem;
border: 0;
transition: background 0.3s ease;
font-family: var(--inter-font);
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.4rem;
letter-spacing: -0.00875rem;
text-decoration: none;
white-space: nowrap;
}
.pill:hover {
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
}
.pill-group .pill:nth-child(6n + 1) {
--pill-accent: var(--bright-blue);
}
.pill-group .pill:nth-child(6n + 2) {
--pill-accent: var(--electric-violet);
}
.pill-group .pill:nth-child(6n + 3) {
--pill-accent: var(--french-violet);
}
.pill-group .pill:nth-child(6n + 4),
.pill-group .pill:nth-child(6n + 5),
.pill-group .pill:nth-child(6n + 6) {
--pill-accent: var(--hot-red);
}
.pill-group svg {
margin-inline-start: 0.25rem;
}
.social-links {
display: flex;
align-items: center;
gap: 0.73rem;
margin-top: 1.5rem;
}
.social-links path {
transition: fill 0.3s ease;
fill: var(--gray-400);
}
.social-links a:hover svg path {
fill: var(--gray-900);
}
@media screen and (max-width: 650px) {
.content {
flex-direction: column;
width: max-content;
}
.divider {
height: 1px;
width: 100%;
background: var(--red-to-pink-to-purple-horizontal-gradient);
margin-block: 1.5rem;
}
}
</style>
<main class="main">
<div class="content">
<div class="left-side">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 982 239"
fill="none"
class="angular-logo"
>
<g clip-path="url(#a)">
<path
fill="url(#b)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
<path
fill="url(#c)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
</g>
<defs>
<radialGradient
id="c"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF41F8" />
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
</radialGradient>
<linearGradient
id="b"
x1="0"
x2="982"
y1="192"
y2="192"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F0060B" />
<stop offset="0" stop-color="#F0070C" />
<stop offset=".526" stop-color="#CC26D5" />
<stop offset="1" stop-color="#7702FF" />
</linearGradient>
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
</defs>
</svg>
<h1>Hello, {{ title() }}</h1>
<p>Congratulations! Your app is running. 🎉</p>
</div>
<div class="divider" role="separator" aria-label="Divider"></div>
<div class="right-side">
<div class="pill-group">
@for (item of [
{ title: 'Explore the Docs', link: 'https://angular.dev' },
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
{ title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
]; track item.title) {
<a
class="pill"
[href]="item.link"
target="_blank"
rel="noopener"
>
<span>{{ item.title }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
height="14"
viewBox="0 -960 960 960"
width="14"
fill="currentColor"
>
<path
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
/>
</svg>
</a>
}
</div>
<div class="social-links">
<a
href="https://github.com/angular/angular"
aria-label="Github"
target="_blank"
rel="noopener"
>
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Github"
>
<path
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
/>
</svg>
</a>
<a
href="https://twitter.com/angular"
aria-label="Twitter"
target="_blank"
rel="noopener"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Twitter"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
/>
</svg>
</a>
<a
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
aria-label="Youtube"
target="_blank"
rel="noopener"
>
<svg
width="29"
height="20"
viewBox="0 0 29 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Youtube"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
/>
</svg>
</a>
</div>
</div>
</div>
</main>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<router-outlet />
+24 -1
View File
@@ -1,3 +1,26 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { KontorComponent } from './kontor/kontor.component';
import { Auth } from './common/auth/auth';
import { ComicSectionComponent } from './kontor/comic/comic-section/comic-section.component';
import { comicRoutes } from './kontor/comic/comic-section/comic-section.routes';
import { TyscSectionComponent } from './kontor/tysc/tysc-section/tysc-section.component';
import { tyscRoutes } from './kontor/tysc/tysc-section/tysc-section.routes';
import { MediaSectionComponent } from './kontor/media/media-section/media-section.component';
import { mediaRoutes } from './kontor/media/media-section/media-section.routes';
export const routes: Routes = []; export const routes: Routes = [
{ path: '', component: KontorComponent, },
{ path: 'login', component: Auth, },
{
path: 'comic', component: ComicSectionComponent,
children: comicRoutes,
},
{
path: 'tysc', component: TyscSectionComponent,
children: tyscRoutes,
},
{
path: 'media', component: MediaSectionComponent,
children: mediaRoutes,
},
];
-2
View File
@@ -1,4 +1,3 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { App } from './app'; import { App } from './app';
@@ -6,7 +5,6 @@ describe('App', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [App], imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents(); }).compileComponents();
}); });
-12
View File
@@ -1,12 +0,0 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {
protected readonly title = signal('kontor-angular');
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth-service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,55 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, Observable, throwError } from 'rxjs';
interface AuthResponseData {
kind: string;
idToken: string;
email: string;
refreshToken: string;
expiresIn: string;
}
export interface TokenData {
access_token: string;
token_type: string;
}
@Injectable({providedIn: 'root'})
export class AuthService {
private http = inject(HttpClient);
signup(email: string, password: string) {
return this.http.post<TokenData>('http://localhost:8800/signup',
{
username: email,
password: password
}
).pipe(catchError(errorRes => {
let errorMessage = 'An unknown error occurred!';
const err = Error(errorMessage);
if (!errorRes.error) {
return throwError(() => err);
}
switch(errorRes.statusText) {
case 'ERROR':
errorMessage = 'Uups...';
}
return throwError(() => Error(errorMessage));
}));
}
login(email: string, password: string): Observable<TokenData> {
const params = new HttpParams()
.set('username', email)
.append('password', password);
return this.http.post<TokenData>('http://127.0.0.1:8800/api/login/token', params,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
}
);
}
}
@@ -0,0 +1,8 @@
button {
text-align: center;
padding: 10px;
border-radius: 10px;
margin-left: 14px;
}
@@ -0,0 +1,34 @@
<div class="row">
<div style="margin-top: 16px;">
@if(error) {
<div class="alert alter-danger">
<p>{{ error }}</p>
</div>
}
@if(isLoading) {
<div style="text-align: center;">
<app-loading-spinner></app-loading-spinner>
</div>
}
@else {
<form (ngSubmit)="onSubmit(authForm)" #authForm="ngForm">
<div class="form-group" style="margin-bottom: 7px;">
<label for="email">E-Mail</label>
<input type="email" id="email" class="form-control" ngModel name="email" required email=""/>
</div>
<div class="form-group" style="margin-bottom: 7px;">
<label for="password">Password</label>
<input type="password" id="password" class="form-control" ngModel name="password" required/>
</div>
<div>
<button class="btn btn-primary" type="submit" [disabled]="!authForm.valid">
{{ isLoginMode ? 'Login' : 'Sign Up' }}
</button>
<button class="btn btn-primary" (click)="onSwitchMode()" type="button">
Switch to {{ isLoginMode ? 'Sign Up' : 'Login' }}
</button>
</div>
</form>
}
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Auth } from './auth';
describe('Auth', () => {
let component: Auth;
let fixture: ComponentFixture<Auth>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Auth]
})
.compileComponents();
fixture = TestBed.createComponent(Auth);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,55 @@
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgForm } from '@angular/forms';
import { AuthService, TokenData } from './auth-service';
import { LoadingSpinner } from "../../shared/loading-spinner/loading-spinner";
import { Observable } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-auth',
imports: [FormsModule, LoadingSpinner],
templateUrl: './auth.html',
styleUrl: './auth.css'
})
export class Auth {
isLoginMode = true;
isLoading = false;
error: string | null = null;
constructor(private authService: AuthService) {}
onSwitchMode() {
this.isLoginMode =!this.isLoginMode;
}
onSubmit(form: NgForm) {
if (!form.valid) {
return;
}
const email = form.value.email;
const password = form.value.password;
let authObservable: Observable<TokenData>;
this.isLoading = true;
if (this.isLoginMode) {
authObservable = this.authService.login(email, password);
} else {
authObservable = this.authService.signup(email, password)
}
authObservable.subscribe({
next: token => {
console.log(token);
localStorage.setItem('userToken', JSON.stringify(token));
this.isLoading = false;
},
error: err => {
console.log(err);
this.isLoading = false;
}
});
form.reset();
}
}
@@ -0,0 +1,5 @@
div {
border-radius: 6px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
@@ -0,0 +1,5 @@
<div>
<a [routerLink]="['/', 'comic', 'artist', artist().id]" routerLinkActive="active">
<span>{{ artist().name }}</span>
</a>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComicArtistComponent } from './comic-artist.component';
describe('ComicArtistComponent', () => {
let component: ComicArtistComponent;
let fixture: ComponentFixture<ComicArtistComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ComicArtistComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ComicArtistComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,13 @@
import { Component, input } from '@angular/core';
import { Artist } from '../comic.model';
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-comic-artist',
imports: [RouterLink, RouterLinkActive],
templateUrl: './comic-artist.component.html',
styleUrl: './comic-artist.component.css'
})
export class ComicArtistComponent {
artist = input.required<Artist>();
}
@@ -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>();
}

Some files were not shown because too many files have changed in this diff Show More