2026-09-21 11:16:52 +02:00
|
|
|
import Elementary
|
|
|
|
|
import Persistence
|
|
|
|
|
|
|
|
|
|
/// A listing in a card grid: its cover, title, price, grade chip, and — depending on the ``Context`` — the shop selling it.
|
|
|
|
|
struct ListingCard: HTML {
|
|
|
|
|
|
|
|
|
|
// MARK: Properties
|
|
|
|
|
|
|
|
|
|
/// Where the card is shown.
|
|
|
|
|
let context: Context
|
|
|
|
|
|
|
|
|
|
/// The listing the card shows.
|
|
|
|
|
let listing: Listing
|
|
|
|
|
|
|
|
|
|
// MARK: Initializers
|
|
|
|
|
|
|
|
|
|
/// Creates a listing card.
|
|
|
|
|
/// - Parameters:
|
|
|
|
|
/// - listing: the listing the card shows.
|
|
|
|
|
/// - context: where the card is shown; a browsing row by default.
|
|
|
|
|
init(
|
|
|
|
|
_ listing: Listing,
|
|
|
|
|
context: Context = .browse
|
|
|
|
|
) {
|
|
|
|
|
self.context = context
|
|
|
|
|
self.listing = listing
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Computed
|
|
|
|
|
|
|
|
|
|
var body: some HTML {
|
|
|
|
|
article(.class("listing-card")) {
|
|
|
|
|
// Saving is a gated action: signed out, it leads to the sign-in screen.
|
|
|
|
|
a(
|
|
|
|
|
.class("listing-card__save"),
|
|
|
|
|
.href(.Route.signIn(returningTo: listing.path)),
|
|
|
|
|
.custom(
|
|
|
|
|
name: "aria-label",
|
|
|
|
|
value: "Save \(listing.title) to watchlist"
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
Icon(.bookmark, size: 18)
|
|
|
|
|
}
|
|
|
|
|
a(
|
|
|
|
|
.class("listing-card__cover"),
|
|
|
|
|
.href(listing.path),
|
|
|
|
|
.custom(
|
|
|
|
|
name: "tabindex",
|
|
|
|
|
value: "-1"
|
|
|
|
|
),
|
|
|
|
|
.custom(
|
|
|
|
|
name: "aria-hidden",
|
|
|
|
|
value: "true"
|
|
|
|
|
)
|
|
|
|
|
) {
|
2026-09-21 12:55:35 +02:00
|
|
|
ListingImage(listing)
|
2026-09-21 11:16:52 +02:00
|
|
|
}
|
|
|
|
|
div {
|
|
|
|
|
h3(.class("type-card-title")) {
|
|
|
|
|
a(
|
|
|
|
|
.class("listing-card__title"),
|
|
|
|
|
.href(listing.path)
|
|
|
|
|
) {
|
|
|
|
|
listing.title
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
p(.class("listing-card__meta")) {
|
|
|
|
|
listing.summary
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
div(.class("listing-card__row")) {
|
|
|
|
|
span(.class("type-price-card")) {
|
|
|
|
|
listing.priceCents.euros
|
|
|
|
|
}
|
|
|
|
|
Chip(
|
|
|
|
|
listing.chip.label,
|
|
|
|
|
tone: listing.chip.tone,
|
|
|
|
|
isCertified: listing.chip.isCertified
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if context != .shop {
|
|
|
|
|
a(
|
|
|
|
|
.class("listing-card__shop"),
|
|
|
|
|
.href(listing.shop.path)
|
|
|
|
|
) {
|
|
|
|
|
context == .search
|
|
|
|
|
? "\(listing.shop.name) · \(listing.shop.country) · ship \(listing.shippingCents.euros)"
|
|
|
|
|
: listing.shop.name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Enumerations
|
|
|
|
|
|
|
|
|
|
extension ListingCard {
|
|
|
|
|
/// Where a listing card is shown.
|
|
|
|
|
enum Context: Sendable {
|
|
|
|
|
/// A browsing row: the shop is named.
|
|
|
|
|
case browse
|
|
|
|
|
/// Search results: the shop is named with its country and shipping cost.
|
|
|
|
|
case search
|
|
|
|
|
/// The shop's own screen: the shop goes unnamed.
|
|
|
|
|
case shop
|
|
|
|
|
}
|
|
|
|
|
}
|