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,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.
|
||||
|
||||
Reference in new issue
Block a user