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

This commit is contained in:
javier committed 2026-09-21 13:32:05 +02:00
1 parent 89a7a7b8e5
commit 31d2d98d49
46 files changed
+4039 -162

No files matched your search

@@ -0,0 +1,112 @@
import Elementary
/// A labelled text field, with its hint or — tied to it for assistive technology — what is wrong with it.
struct TextField: HTML {
// MARK: Properties
let name: String
let title: String
var value: String = ""
var placeholder: String = ""
var hint: String?
var problem: String?
var isWide = false
var maxLength: Int?
// MARK: Computed
var body: some HTML {
div(.class(isWide ? "field field--wide" : "field")) {
label(
.class("field__label"),
.for(name)
) {
title
}
input(
.class("input"),
.id(name),
.type(.text),
.name(name),
.value(value),
.placeholder(placeholder)
)
.attributes(.custom(name: "maxlength", value: "\(maxLength ?? 0)"), when: maxLength != nil)
.attributes(
.custom(name: "aria-invalid", value: "true"),
.custom(name: "aria-describedby", value: "\(name)-error"),
when: problem != nil
)
FieldNote(name: name, hint: hint, problem: problem)
}
}
}
/// A labelled select, showing the chosen option.
struct SelectField: HTML {
// MARK: Properties
let name: String
let title: String
let options: [(value: String, label: String)]
var selected: String?
var problem: String?
// MARK: Computed
var body: some HTML {
div(.class("field")) {
label(
.class("field__label"),
.for(name)
) {
title
}
select(
.class("select select--block"),
.id(name),
.name(name)
) {
ForEach(options) { option in
Elementary.option(.value(option.value)) {
option.label
}
.attributes(.selected, when: option.value == selected)
}
}
FieldNote(name: name, hint: nil, problem: problem)
}
}
}
/// What is wrong with a field, under it; or, when nothing is, its hint.
struct FieldNote: HTML {
// MARK: Properties
let name: String
let hint: String?
let problem: String?
// MARK: Computed
var body: some HTML {
if let problem {
p(
.class("field__error"),
.id("\(name)-error")
) {
problem
}
} else if let hint {
p(.class("field__hint")) {
hint
}
}
}
}
@@ -30,13 +30,13 @@ struct ListingCard: HTML {
var body: some HTML {
article(.class("listing-card")) {
// Saving is a gated action: signed out, it leads to the sign-in screen.
// Saving a copy adds a want for it; signed out, the want list leads to the sign-in screen first.
a(
.class("listing-card__save"),
.href(.Route.signIn(returningTo: listing.path)),
.href(.Route.newWant(listing: listing.slug)),
.custom(
name: "aria-label",
value: "Save \(listing.title) to watchlist"
value: "Add \(listing.title) to your want list"
)
) {
Icon(.bookmark, size: 18)
@@ -45,17 +45,6 @@ struct SiteFooter: HTML {
span {
"CCN B.V. · Amsterdam · KvK [NUMBER] · BTW-id [NUMBER] · Business sellers show their KvK number on their shop page; consumers have a 14-day right of withdrawal on purchases from business sellers."
}
span(.class("site-footer__legal-links")) {
a(.href(.Route.terms)) {
"Terms"
}
a(.href(.Route.privacy)) {
"Privacy"
}
a(.href(.Route.sellerRules)) {
"Seller rules"
}
}
}
}
}
@@ -88,7 +77,6 @@ private extension SiteFooter {
("Open a shop", .Route.openShop),
("Seller dashboard", .Route.sellerDashboard),
("Grading guide", .Route.gradingGuide),
("Trade at events", .Route.events),
]
),
(
@@ -96,10 +84,8 @@ private extension SiteFooter {
links: [
("Sign in or join", .Route.signIn),
("Want list & alerts", .Route.wants),
("My collection", .Route.collection),
("Messages", .Route.messages),
("Passkeys & settings", .Route.account),
("Lost your passkey?", .Route.recover),
]
),
]
@@ -98,7 +98,7 @@ private extension SiteHeader {
}
}
/// The format links, plus the series and events links shown on tablet only.
/// The format links, plus the series link shown on tablet only.
var navigation: some HTML {
nav(
.class("site-nav"),
@@ -134,12 +134,6 @@ private extension SiteHeader {
) {
"Series"
}
a(
.class("site-nav__link site-nav__link--extra"),
.href(.Route.events)
) {
"Events"
}
}
}
@@ -193,7 +187,7 @@ private extension SiteHeader {
.bell,
label: "Want list",
count: viewer.newWantMatches,
unit: "new matches",
unit: viewer.newWantMatches == 1 ? "new match" : "new matches",
path: .Route.wants
)
counter(
@@ -203,13 +197,11 @@ private extension SiteHeader {
unit: "unread",
path: .Route.messages
)
if viewer.hasShop {
a(
.class("site-header__text-link site-header__text-link--strong from-tablet"),
.href(.Route.sellerDashboard)
) {
"My shop"
}
a(
.class("site-header__text-link site-header__text-link--strong from-tablet"),
.href(viewer.hasShop ? .Route.sellerDashboard : .Route.openShop)
) {
viewer.hasShop ? "My shop" : "Open a shop"
}
a(
.class("site-header__account"),
@@ -226,19 +218,6 @@ private extension SiteHeader {
viewer.initials
}
}
// Not in the design, whose sign-out lives on the account screen the prototype leaves out.
form(
.class("site-header__sign-out"),
.action(.Route.signOut),
.method(.post)
) {
button(
.class("site-header__text-link"),
.type(.submit)
) {
"Sign out"
}
}
}
/// An icon link with a count badge, omitted at zero.