Files
ccn/Services/Website/Sources/Library/Internal/Pages/ComparePage.swift
T
javierandClaude Opus 5.5 911424de24 Project updates from Template
This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing:

- Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305)
- Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df)
- Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9)
- Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769)
- Enabled the Analytics trait in the Website service. (f6a6bbd)
- Reworked page routing in the Website service. (d1cb2c1)
- Gated the structured data features in the Infrastructure package behind a trait. (f0a8791)
- Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3)
- Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b)

The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead.

The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:55:43 +01:00

196 lines
7.2 KiB
Swift

import Elementary
import Foundation
import Infrastructure
import Persistence
/// Copies side by side: grade, page quality, declared defects, and what each costs delivered to the Netherlands.
struct ComparePage {
// MARK: Properties
let analytics: Analytics?
let assetVersion: String?
let locale: Locale
/// The copies compared, cheapest delivered first.
let copies: [Listing]
/// The signed-in viewer, or `nil` when signed out.
let viewer: Viewer?
// MARK: Initializers
/// Creates the comparison.
/// - Parameters:
/// - copies: the copies compared, in any order.
/// - 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.
init(
copies: [Listing],
viewer: Viewer? = nil,
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.copies = copies.sorted { Self.delivered($0) < Self.delivered($1) }
self.locale = .init(identifier: "en")
self.viewer = viewer
}
}
// MARK: - Page
extension ComparePage: Page {
// MARK: Computed
var content: some HTML {
SiteLayout(viewer: viewer) {
div(.class("page compare")) {
div(.class("compare__head")) {
div(.class("series__intro")) {
p(.class("type-eyebrow")) {
"Compare · \(copies.first?.title ?? "copies") · \(copies.count) \(copies.count == 1 ? "copy" : "copies")"
}
h1(.class("type-h1")) {
"Which copy?"
}
p(.class("type-body-sm compare__lead")) {
"Grade, page quality, declared defects and total cost delivered to the Netherlands, side by side."
}
}
if let first = copies.first {
a(
.class("button button--outline"),
.href(.Route.series(first.seriesSlug, issue: first.issueNumber))
) {
"Back to the series"
}
}
}
if copies.isEmpty {
div(.class("empty")) {
h2(.class("type-panel")) {
"Nothing to compare"
}
p(.class("type-body-sm")) {
"Tick up to \(SeriesPage.maxCompared) copies of an issue on its series page."
}
a(
.class("button button--outline"),
.href(.Route.series)
) {
"Browse series"
}
}
}
ul(.class("compare__copies")) {
ForEach(copies) { copy in
li(.class("card compare__copy")) {
a(
.class("compare__cover"),
.href(copy.path),
.custom(name: "aria-label", value: "\(copy.title), \(copy.chip.label), from \(copy.shop.name)")
) {
ListingImage(copy)
}
div(.class("compare__price")) {
span(.class("type-stat")) {
Self.delivered(copy).euros
}
Chip(copy.chip.label, tone: copy.chip.tone, isCertified: copy.chip.isCertified)
}
dl(.class("compare__facts")) {
ForEach(Self.facts(of: copy)) { fact in
dt {
fact.term
}
dd {
fact.value
}
}
}
div(.class("compare__actions")) {
a(
.class("button button--primary button--sm"),
.href(.Route.gated(.Route.buy(copy.slug), from: copy.path, viewer: viewer))
) {
"Buy now"
}
a(
.class("button button--outline button--sm"),
.href(copy.path)
) {
"View listing"
}
}
}
}
}
p(.class("gate-note")) {
Icon(.info, size: 16)
span {
"The delivered price adds each seller's tracked shipping to the Netherlands. Sales from outside the EU may attract import VAT at delivery."
}
}
}
}
}
@HTMLBuilder
var metadata: some HTML {
meta(
.name("robots"),
.content("noindex")
)
siteMetadata
}
var scripts: [any Asset] {
[StaticFile.shared]
}
var stylesheets: [any Asset] {
[StaticFile.shared, StaticFile.collecting]
}
var title: String {
"Compare copies — CCN"
}
}
// MARK: - Helpers
private extension ComparePage {
/// What a copy costs delivered to the Netherlands, in euro cents.
static func delivered(
_ copy: Listing
) -> Int {
copy.priceCents + copy.shippingCents
}
/// What sets a copy apart, row by row.
static func facts(
of copy: Listing
) -> [(term: String, value: String)] {
let customs = copy.shop.shipsFromEU ? "" : " · customs may apply"
let rating = copy.shop.rating.formatted(.number.precision(.fractionLength(1)).locale(.init(identifier: "en")))
return [
("Asking", copy.priceCents.euros + (copy.acceptsOffers ? " · accepts offers" : "")),
("Page quality", copy.pageQuality ?? "Not stated"),
("Printing", copy.printingDetail),
("Declared defects", copy.defects.isEmpty ? "None declared" : copy.defects.joined(separator: "; ")),
("Ship to NL", "\(copy.shippingCents.euros) tracked · \(copy.shop.handlingDays)-day handling\(customs)"),
("Shop", "\(copy.shop.name) · \(copy.shop.location)"),
("Rated sales", copy.shop.ratedSales == 0 ? "None yet" : "\(copy.shop.ratedSales) · \(rating) · \(copy.shop.returnsNote)"),
]
}
}