Implemented phase 1 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 11:16:52 +02:00
1 parent 6d7ab4bcba
commit ea89ea892a
37 files changed
+4173 -43

No files matched your search

@@ -2,6 +2,7 @@ import Elementary
import Foundation
import Infrastructure
import Localization
import Persistence
/// The website's landing page, with its text localized to a given locale.
struct IndexPage {
@@ -14,6 +15,9 @@ struct IndexPage {
/// The version token appended to the page's asset URLs, or `nil` to leave them unversioned.
let assetVersion: String?
/// The listings the page shows in its rows.
let listings: HomeListings
/// The locale the page content is localized to.
let locale: Locale
@@ -31,14 +35,17 @@ struct IndexPage {
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
/// - siteOrigin: the public origin the page derives its canonical URL and language alternates from, or `nil` (the default) to omit them.
/// - analytics: the analytics tracker embedded in the document head, or `nil` (the default) to omit it.
/// - listings: the listings the page shows in its rows; none by default.
init(
locale: Locale,
assetVersion: String? = nil,
siteOrigin: String? = nil,
analytics: Analytics? = nil
analytics: Analytics? = nil,
listings: HomeListings = .init()
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.listings = listings
self.locale = locale
self.siteOrigin = siteOrigin
self.localize = .init(bundle: .module)
@@ -82,32 +89,85 @@ extension IndexPage: Page {
// MARK: Properties
/// The hero alone, until the listings exist.
var content: some HTML {
SiteLayout {
div(.class("page")) {
div(.class("page home")) {
section(.class("hero")) {
p(.class("type-eyebrow")) {
"Single issues · Manga · Trades · Hardcovers"
}
h1(.class("type-hero")) {
localize("index.heading", locale: locale)
}
p(.class("type-lead")) {
"Search every listing by title, print and grade. Browse any seller's shop without an account — sign in with a passkey only when you're ready to buy, make an offer or message."
}
div(.class("hero__actions")) {
a(
.class("button button--primary button--lg"),
.href(Format.singleIssues.path)
) {
"Browse single issues"
div(.class("hero__text")) {
p(.class("type-eyebrow")) {
"Single issues · Manga · Trades · Hardcovers"
}
h1(.class("type-hero")) {
localize("index.heading", locale: locale)
}
p(.class("type-lead")) {
"Search every listing by title, print and grade. Browse any seller's shop without an account — sign in with a passkey only when you're ready to buy, make an offer or message."
}
div(.class("hero__actions")) {
a(
.class("button button--primary button--lg"),
.href(Format.singleIssues.path)
) {
"Browse single issues"
}
a(
.class("button button--outline button--lg"),
.href(.Route.series)
) {
"Browse by series"
}
}
}
if listings.singleIssues.count >= 3 {
div(.class("hero__covers")) {
Cover(listings.singleIssues[1].cover, modifier: "cover--left")
Cover(listings.singleIssues[0].cover, modifier: "cover--centre")
Cover(listings.singleIssues[2].cover, modifier: "cover--right")
}
}
}
nav(
.class("formats"),
.custom(name: "aria-label", value: "Browse by format")
) {
for format in Format.allCases {
a(
.class("button button--outline button--lg"),
.href(.Route.series)
.class("formats__tile"),
.href(format.path)
) {
"Browse by series"
span(.class("formats__name")) {
format.label
}
span(.class("type-caption")) {
format.summary
}
}
}
}
row(
"New in single issues",
more: "All single issues",
path: Format.singleIssues.path,
listings: listings.singleIssues
)
row(
"Manga, trades and hardcovers",
more: "All collected editions",
path: SearchParameters(items: Format.allCases.dropFirst().map {
.init(name: SearchParameters.Name.format.rawValue, value: $0.rawValue)
}).path(.Route.search),
listings: listings.collected
)
section(.class("promises")) {
for promise in Self.promises {
div(.class("promises__item")) {
Icon(promise.glyph, size: 28, weight: "1.8")
h3(.class("promises__title")) {
promise.title
}
p(.class("type-body-sm promises__text")) {
promise.text
}
}
}
}
@@ -128,3 +188,50 @@ extension IndexPage: Page {
}
}
// MARK: - Helpers
private extension IndexPage {
// MARK: Constants
/// What the marketplace promises, under the listing rows.
static let promises: [(glyph: Icon.Glyph, title: String, text: String)] = [
(.key, "Passkeys, not passwords", "Sign up and sign in with Face ID, a fingerprint or a hardware key. Nothing to phish, nothing to reuse, nothing to reset."),
(.document, "Grade stated up front", "Every listing declares slab or raw, grader and grade, printing and defects, with photos of the actual copy front and back."),
(.shop, "Shops are open to everyone", "Anyone can browse a seller's inventory without an account. Buying, offers and messages need a signed-in account on both sides."),
]
// MARK: Methods
/// A titled row of listing cards; renders nothing without listings.
@HTMLBuilder
func row(
_ title: String,
more: String,
path: String,
listings: [Listing]
) -> some HTML {
if !listings.isEmpty {
section(.class("home__row")) {
div(.class("section-head")) {
h2(.class("type-h2")) {
title
}
a(
.class("link-ink"),
.href(path)
) {
more
}
}
div(.class("card-grid")) {
for listing in listings {
ListingCard(listing)
}
}
}
}
}
}