import Elementary import Foundation import Infrastructure import Persistence /// The events where shops trade in person: agree on the site, skip shipping, collect the copy at the con. struct EventsPage { // MARK: Properties let analytics: Analytics? let assetVersion: String? let locale: Locale /// The events still to come, soonest first, each with the number of listings that can be picked up there. let events: [(event: Event, listingCount: Int)] /// The moment "this weekend" is judged from. let now: Date /// The signed-in viewer, or `nil` when signed out. let viewer: Viewer? // MARK: Initializers /// Creates the events screen. /// - Parameters: /// - events: the events still to come, soonest first. /// - listings: every listing for sale, counted per event by the shops trading there. /// - viewer: the signed-in viewer, or `nil` (the default) when signed out. /// - now: the moment "this weekend" is judged from; the present by default. /// - 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( events: [Event], listings: [Listing], viewer: Viewer? = nil, now: Date = .init(), assetVersion: String? = nil, analytics: Analytics? = nil ) { self.analytics = analytics self.assetVersion = assetVersion self.events = events.map { event in let shops = Set(event.shops.map(\.slug)) return (event, listings.count { shops.contains($0.shop.slug) }) } self.locale = .init(identifier: "en") self.now = now self.viewer = viewer } } // MARK: - Page extension EventsPage: Page { // MARK: Computed var content: some HTML { SiteLayout(viewer: viewer) { div(.class("page wants")) { div(.class("events__intro")) { h1(.class("type-h1")) { "Pick up at a con" } p(.class("type-lead")) { "Sellers mark which events they'll trade at. Agree on CCN, skip shipping, collect it in person." } } if events.isEmpty { div(.class("empty")) { h2(.class("type-panel")) { "No events coming up" } p(.class("type-body-sm")) { "Every copy still ships, tracked." } } } ul(.class("wants__list")) { ForEach(events) { entry in li { a( .class("card events__event"), .href(.Route.pickup(at: entry.event.slug)) ) { span(.class("wants__top")) { span(.class("events__name")) { strong { entry.event.name } span(.class("type-small")) { Date.span(from: entry.event.startsOn, to: entry.event.endsOn) } } if entry.event.startsOn.isWithinAWeek(of: now) { Chip("This weekend", tone: .warn) } } span(.class("type-small")) { entry.event.venue } span(.class("events__chips")) { Chip("\(entry.event.shops.count) \(entry.event.shops.count == 1 ? "shop" : "shops") trading") Chip("\(entry.listingCount) \(entry.listingCount == 1 ? "listing" : "listings") for pickup") } } } } } p(.class("type-small wants__note")) { "Selling? Pick the events you'll trade at on your seller dashboard." } } } } var scripts: [any Asset] { [StaticFile.shared] } var stylesheets: [any Asset] { [StaticFile.shared, StaticFile.trade] } var summary: String? { "Comic cons where CCN shops trade in person: agree on a copy online, collect it at the event." } var title: String { "Events — CCN" } }