Implemented phase 5 for the POC prototype in the Website service.
This commit is contained in:
1 parent
89a7a7b8e5
commit
31d2d98d49
46 files changed
+4039
-162
No files matched your search
@@ -0,0 +1,194 @@
|
||||
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)"),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user