Implemented phase 6 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 13:53:13 +02:00
1 parent 256d22df40
commit f2f445827b
40 files changed
+1906 -37

No files matched your search

@@ -18,6 +18,9 @@ struct SeriesPage {
/// Every listing of the series still for sale.
let listings: [Listing]
/// The issues the viewer owns of the series, or `nil` signed out.
let owned: Set<Int>?
/// The series shown.
let series: Series
@@ -31,6 +34,7 @@ struct SeriesPage {
/// - series: the series shown.
/// - listings: every listing of the series still for sale.
/// - issue: the issue whose copies are listed, or `nil` (the default) for none.
/// - owned: the issues the viewer owns of the series, or `nil` (the default) signed out.
/// - viewer: the signed-in viewer, or `nil` (the default) when signed out.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
/// - analytics: the analytics tracker embedded in the document head, or `nil` (the default) to omit it.
@@ -38,6 +42,7 @@ struct SeriesPage {
series: Series,
listings: [Listing],
issue: Int? = nil,
owned: Set<Int>? = nil,
viewer: Viewer? = nil,
assetVersion: String? = nil,
analytics: Analytics? = nil
@@ -47,6 +52,7 @@ struct SeriesPage {
self.issue = issue
self.listings = listings
self.locale = .init(identifier: "en")
self.owned = owned
self.series = series
self.viewer = viewer
}
@@ -104,6 +110,19 @@ extension SeriesPage: Page {
p(.class("type-lead")) {
"\(series.blurb)\(series.blurb.isEmpty ? "" : " ")\(listings.count) \(listings.count == 1 ? "copy" : "copies") listed across CCN right now."
}
if let owned, series.issueCount > 0 {
div(.class("card series__collection")) {
p(.class("type-eyebrow")) {
"Your collection"
}
p(.class("type-panel")) {
"\(owned.count) of \(series.issueCount) owned"
}
p(.class("type-small")) {
"\(gaps.count) missing · \(gaps.filter { !$0.copies.isEmpty }.count) of them listed now"
}
}
}
ul(.class("series__keys")) {
ForEach(keys) { key in
li(.class("chip chip--warn")) {
@@ -125,7 +144,7 @@ extension SeriesPage: Page {
}
ol(.class("series__issues")) {
ForEach(issues) { entry in
li(.class(entry.copies.isEmpty ? "series__issue series__issue--gap" : "series__issue")) {
li(.class(issueClass(entry))) {
span(.class("series__number")) {
"#\(entry.number)"
}
@@ -143,6 +162,25 @@ extension SeriesPage: Page {
"None listed · alert me"
}
}
if let owned {
form(
.action("\(String.Route.series(series.slug))/own"),
.method(.post)
) {
input(
.type(.hidden),
.name("issue"),
.value("\(entry.number)")
)
button(
.class("series__own"),
.type(.submit),
.custom(name: "aria-pressed", value: owned.contains(entry.number) ? "true" : "false")
) {
owned.contains(entry.number) ? "Owned ✓" : "I own this"
}
}
}
}
}
}
@@ -274,6 +312,24 @@ extension SeriesPage: Page {
private extension SeriesPage {
/// The issues of the run the viewer does not own.
var gaps: [(number: Int, copies: [Listing])] {
issues.filter { !(owned ?? []).contains($0.number) }
}
/// An issue's tile: a gap reads quieter, and an owned issue is dimmed.
func issueClass(
_ entry: (number: Int, copies: [Listing])
) -> String {
[
"series__issue",
entry.copies.isEmpty ? "series__issue--gap" : nil,
owned?.contains(entry.number) == true ? "series__issue--owned" : nil,
]
.compactMap(\.self)
.joined(separator: " ")
}
/// The series' manga, trade paperbacks, and hardcovers.
var collected: [Listing] {
listings.filter { $0.issueNumber == nil }