Extend display of comics publishers

This commit is contained in:
Thomas Peetz
2025-10-03 16:04:52 +02:00
parent a9829bf118
commit c52a5b8715
23 changed files with 327 additions and 33 deletions
@@ -8,29 +8,28 @@
<h2>{{ artist().name }}</h2> <h2>{{ artist().name }}</h2>
<a href="{{ artist().weblink }}" style="background-color: green;">{{ artist().name }}</a> <a href="{{ artist().weblink }}" style="background-color: green;">{{ artist().name }}</a>
</section> </section>
<section>
@for (work of artist().comic_works; track work.worktype.id) { @for (work of artist().comic_works; track work.worktype.id) {
<section>
<app-comic-worktype [worktype]="work.worktype"/> <app-comic-worktype [worktype]="work.worktype"/>
@for (comic of work.comics; track comic.id) { @for (comic of work.comics; track comic.id) {
<article> <article>
<app-comic-comic [comic]="comic"/> <app-comic-comic [comic]="comic"/>
</article> </article>
} }
</section>
} }
</section>
<section>
@for (work of artist().issue_works; track work.worktype.id) { @for (work of artist().issue_works; track work.worktype.id) {
<section>
<app-comic-worktype [worktype]="work.worktype"/> <app-comic-worktype [worktype]="work.worktype"/>
@for (issue of work.issues; track issue.id) { @for (issue of work.issues; track issue.id) {
<article> <article>
<app-comic-comic [comic]="issue.comic"/> <app-comic-comic [comic]="issue.comic"/>
</article> </article>
} }
</section>
} }
</section>
} @else { } @else {
<h2>Artist Details</h2> <h2>Artist Details</h2>
} }
</div> </div>
</div> </div>
@@ -7,17 +7,23 @@
<section> <section>
<h2>{{ comic().title }}</h2> <h2>{{ comic().title }}</h2>
<a href="{{ comic().weblink }}" style="background-color: green;">{{ comic().title }}</a> <a href="{{ comic().weblink }}" style="background-color: green;">{{ comic().title }}</a>
<app-comic-publisher [publisher]="comic().publisher" />
</section> </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) { @for (work of comic().works; track work.worktype.id) {
<section>
<app-comic-worktype [worktype]="work.worktype"/> <app-comic-worktype [worktype]="work.worktype"/>
@for (artist of work.artists; track artist.id) { @for (artist of work.artists; track artist.id) {
<article> <article>
<app-comic-artist [artist]="artist"/> <app-comic-artist [artist]="artist"/>
</article> </article>
} }
</section>
} }
</section>
} @else { } @else {
<h2>Comic Details</h2> <h2>Comic Details</h2>
} }
@@ -5,10 +5,12 @@ import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular
import { ComicService } from './comic.service'; import { ComicService } from './comic.service';
import { ComicWorktypeComponent } from '../comic-worktype/comic-worktype.component'; import { ComicWorktypeComponent } from '../comic-worktype/comic-worktype.component';
import { ComicArtistComponent } from '../comic-artist/comic-artist.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({ @Component({
selector: 'app-comic-comics', selector: 'app-comic-comics',
imports: [ComicComicsListComponent, ComicWorktypeComponent, ComicArtistComponent], imports: [ComicComicsListComponent, ComicWorktypeComponent, ComicArtistComponent, ComicIssueComponent, ComicPublisherComponent],
templateUrl: './comic-comics.component.html', templateUrl: './comic-comics.component.html',
styleUrl: './comic-comics.component.css' styleUrl: './comic-comics.component.css'
}) })
@@ -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>
<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();
});
}
}
@@ -1 +1,14 @@
<p>comic-publishers works!</p> <div class="grid-container">
<div>
<app-comic-publishers-list />
</div>
<div>
@if (publisher()) {
<section>
<h2>{{ publisher().name }}</h2>
</section>
} @else {
<h2>Publisher Details</h2>
}
</div>
</div>
@@ -1,11 +1,23 @@
import { Component } from '@angular/core'; 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';
@Component({ @Component({
selector: 'app-comic-publishers', selector: 'app-comic-publishers',
imports: [], imports: [ComicPublishersListComponent],
templateUrl: './comic-publishers.component.html', templateUrl: './comic-publishers.component.html',
styleUrl: './comic-publishers.component.css' styleUrl: './comic-publishers.component.css'
}) })
export class ComicPublishersComponent { export class ComicPublishersComponent {
publisher = input.required<Publisher>();
} }
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));
})
);
}
}
@@ -1,6 +1,6 @@
import { Routes } from "@angular/router"; import { Routes } from "@angular/router";
import { artistResolver, ComicArtistsComponent } from "../comic-artists/comic-artists.component"; import { artistResolver, ComicArtistsComponent } from "../comic-artists/comic-artists.component";
import { ComicPublishersComponent } from './../comic-publishers/comic-publishers.component'; import { ComicPublishersComponent, publisherResolver } from './../comic-publishers/comic-publishers.component';
import { ComicComicsComponent, comicResolver } from "../comic-comics/comic-comics.component"; import { ComicComicsComponent, comicResolver } from "../comic-comics/comic-comics.component";
export const comicRoutes: Routes = [ export const comicRoutes: Routes = [
@@ -20,8 +20,11 @@ export const comicRoutes: Routes = [
component: ComicPublishersComponent component: ComicPublishersComponent
}, },
{ {
path: 'publishers/:publisherId', path: 'publisher/:publisherId',
component: ComicPublishersComponent, component: ComicPublishersComponent,
resolve: {
publisher: publisherResolver
}
}, },
{ {
path: 'artist', path: 'artist',
@@ -1,5 +1,3 @@
export interface Artist { export interface Artist {
id: string; id: string;
name: string; name: string;
@@ -10,6 +8,16 @@ export interface Worktype {
name: string; name: string;
} }
export interface Publisher {
id: string;
name: string;
}
export interface PublisherDetails {
id: string;
name: string;
}
export interface Comic { export interface Comic {
id: string; id: string;
title: string; title: string;
@@ -22,18 +30,16 @@ export interface Volume {
} }
export interface Issue { export interface Issue {
id: string; id: string;
issue_number: string; issue_number: string;
in_stock: boolean; in_stock: boolean;
is_read: boolean; is_read: boolean;
comic: Comic; comic: Comic;
volume: Volume | undefined volume: Volume;
} }
export interface ComicWork { export interface ComicWork {
worktype: string; worktype: string;
} }
export interface ComicWorktypeArtists { export interface ComicWorktypeArtists {
@@ -48,10 +54,9 @@ export interface ComicDetails {
completed: boolean; completed: boolean;
current_order: boolean; current_order: boolean;
weblink: string; weblink: string;
publisher: string; publisher: Publisher;
volumes: [ issues: Issue[];
name: string, volumes: Volume[];
];
works: ComicWorktypeArtists[]; works: ComicWorktypeArtists[];
} }
+37 -5
View File
@@ -1,16 +1,25 @@
from typing import List from typing import List
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from src.apis.utils import SessionDep from src.apis.utils import SessionDep
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.comic import Artist, Comic, Issue, Publisher
from src.db.repository.comics.artist import get_artist_details from src.db.repository.comics.artist import get_artist_details
from src.db.repository.comics.comic import get_comic_details, get_short_info, list_comics, get_issue_details from src.db.repository.comics.comic import (
from src.schema.comics.artist_details import ArtistDetailResponse get_comic_details,
from src.schema.comics.comic import ComicResponse get_issue_details,
get_short_info,
list_comics,
)
from src.db.repository.comics.publisher import get_publisher_details
from src.schema.comics.artist import ArtistCreation, ArtistResponse from src.schema.comics.artist import ArtistCreation, ArtistResponse
from src.db.models.comic import Comic, Artist, Issue from src.schema.comics.artist_details import ArtistDetailResponse
from src.schema.comics.comic import ComicResponse
from src.schema.comics.comic_details import ComicDetailsResponse from src.schema.comics.comic_details import ComicDetailsResponse
from src.schema.comics.issue_details import IssueDetailsResponse from src.schema.comics.issue_details import IssueDetailsResponse
from src.schema.comics.publisher import PublisherResponse
from src.schema.comics.publisher_details import PublisherDetailsResponse
router = APIRouter() router = APIRouter()
@@ -24,6 +33,7 @@ def get_all_comics(db: SessionDep) -> List[ComicResponse]:
results.append(response) results.append(response)
return results return results
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse) @router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
def get_comic(comic_id: str, db: SessionDep) -> ComicDetailsResponse: def get_comic(comic_id: str, db: SessionDep) -> ComicDetailsResponse:
comic = db.get(Comic, comic_id) comic = db.get(Comic, comic_id)
@@ -34,14 +44,16 @@ def get_comic(comic_id: str, db: SessionDep) -> ComicDetailsResponse:
logger.info(f"ComicDetailsResponse: {response}") logger.info(f"ComicDetailsResponse: {response}")
return response return response
@router.get("/artists", response_model=List[ArtistResponse]) @router.get("/artists", response_model=List[ArtistResponse])
def get_all_artists(db: SessionDep) -> List[ArtistResponse]: def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
results: List[ArtistResponse] = [] results: List[ArtistResponse] = []
artists = db.query(Artist).all() artists = db.query(Artist).all()
for artist in artists: for artist in artists:
results.append(ArtistResponse(id=artist.id, name=str(artist.name))) results.append(ArtistResponse(id=artist.id, name=str(artist.name))) # type: ignore
return results return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse) @router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse: def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id) artist = db.get(Artist, artist_id)
@@ -50,6 +62,7 @@ def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
response: ArtistDetailResponse = get_artist_details(artist) response: ArtistDetailResponse = get_artist_details(artist)
return response return response
@router.post("/artists", status_code=status.HTTP_201_CREATED) @router.post("/artists", status_code=status.HTTP_201_CREATED)
def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse: def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse:
artist: Artist = Artist() artist: Artist = Artist()
@@ -62,6 +75,25 @@ def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistRespons
response = ArtistResponse(id=artist.id, name=str(artist.name)) response = ArtistResponse(id=artist.id, name=str(artist.name))
return response return response
@router.get("/publishers", response_model=List[PublisherResponse])
def get_all_publishers(db: SessionDep) -> List[PublisherResponse]:
results: List[PublisherResponse] = []
publishers = db.query(Publisher).all()
for publisher in publishers:
results.append(PublisherResponse(id=publisher.id, name=str(publisher.name)))
return results
@router.get("/publishers/{publisher_id}", response_model=PublisherDetailsResponse)
def get_publisher(publisher_id: str, db: SessionDep) -> PublisherDetailsResponse:
publisher = db.get(Publisher, publisher_id)
if publisher is None:
raise HTTPException(status_code=404, detail="Publisher could not be found")
response: PublisherDetailsResponse = get_publisher_details(publisher)
return response
@router.get("/issues", response_model=List[IssueDetailsResponse]) @router.get("/issues", response_model=List[IssueDetailsResponse])
def get_issues(db: SessionDep) -> List[IssueDetailsResponse]: def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
results: List[IssueDetailsResponse] = [] results: List[IssueDetailsResponse] = []
@@ -0,0 +1,7 @@
from src.db.models.comic import Publisher
from src.schema.comics.publisher_details import PublisherDetailsResponse
def get_publisher_details(publisher: Publisher):
response: PublisherDetailsResponse = PublisherDetailsResponse(id=publisher.id, name=str(publisher.name))
return response
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class PublisherDetailsResponse(BaseModel):
id: str
name: str