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" 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) · \(String(format: "%.1f", copy.shop.rating)) · \(copy.shop.returnsNote)"), ] } }