Files
ccn/Services/Website/Sources/Library/Internal/Pages/OpenShopPage.swift
T

275 lines
11 KiB
Swift

import Elementary
import Foundation
import Infrastructure
import Persistence
/// The open-a-shop form: the shop's public identity, how its owner sells, and its policies. One screen; the first listing follows.
struct OpenShopPage {
// MARK: Properties
let analytics: Analytics?
let assetVersion: String?
let locale: Locale
/// The values the form is filled with.
let form: ShopForm
/// What is wrong with the form, by field.
let problems: [ShopForm.Field: String]
/// The signed-in viewer.
let viewer: Viewer?
// MARK: Initializers
/// Creates the open-a-shop form.
/// - Parameters:
/// - form: the values the form is filled with; an empty form by default.
/// - problems: what is wrong with the form, by field; nothing by default.
/// - viewer: the signed-in viewer.
/// - 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(
form: ShopForm = .init(),
problems: [ShopForm.Field: String] = [:],
viewer: Viewer? = nil,
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.form = form
self.locale = .init(identifier: "en")
self.problems = problems
self.viewer = viewer
}
}
// MARK: - Page
extension OpenShopPage: Page {
// MARK: Computed
var content: some HTML {
SiteLayout(viewer: viewer) {
div(.class("page listing-form")) {
div(.class("listing-form__head")) {
p(.class("type-eyebrow")) {
"Open a shop · one screen, about five minutes"
}
h1(.class("type-h1")) {
"Set up your shop"
}
p(.class("type-lead")) {
"A shop is a public page plus your policies. You can list the first book right after."
}
}
if !problems.isEmpty {
p(
.class("notice notice--bad"),
.custom(name: "role", value: "alert")
) {
strong {
problems.count == 1 ? "One thing to fix before opening." : "\(problems.count) things to fix before opening."
}
}
}
Elementary.form(
.class("listing-form__form"),
.action(.Route.openShop),
.method(.post)
) {
section(.class("card listing-form__section")) {
h2(.class("type-panel")) {
"1. Shop"
}
div(.class("listing-form__grid")) {
field(.name, "Shop name", placeholder: "Kestrel Row Comics", isWide: true)
field(.handle, "Handle", placeholder: "kestrel-row", hint: "Your page: /shop/your-handle")
field(.city, "Ships from", placeholder: "Amsterdam", hint: "Sets the ships-from filter", isWide: true)
SelectField(
name: ShopForm.Field.country.rawValue,
title: "Country",
options: ShopForm.countries.map { ($0.0, $0.1) },
selected: form.value(.country),
problem: problems[.country]
)
}
div(.class("field")) {
label(
.class("field__label"),
.for("about")
) {
"About the shop (shown at the top of your page)"
}
textarea(
.class("input"),
.id("about"),
.name(ShopForm.Field.about.rawValue),
.custom(name: "rows", value: "3"),
.custom(name: "maxlength", value: "600")
) {
form.value(.about) ?? ""
}
FieldNote(name: "about", hint: nil, problem: problems[.about])
}
}
section(.class("card listing-form__section")) {
div(.class("section-head")) {
h2(.class("type-panel")) {
"2. Private or business seller"
}
span(.class("type-small")) {
"Required by Dutch consumer law for business sellers"
}
}
fieldset(.class("listing-form__modes")) {
legend(.class("visually-hidden")) {
"Seller type"
}
kind(.privateSeller, "Private collector", "Selling your own books now and then. No KvK needed. Buyers get no statutory right of withdrawal.")
kind(.business, "Business (eenmanszaak, VOF, BV…)", "KvK shown on your shop page; consumers get the 14-day right of withdrawal by law.")
}
FieldNote(name: "kind", hint: nil, problem: problems[.kind])
div(.class("listing-form__grid")) {
field(.kvk, "KvK number", placeholder: "12345678", hint: "Business sellers only")
field(.vat, "BTW-id", placeholder: "NL123456789B01", hint: "Optional")
field(.legalName, "Legal name", placeholder: "As registered")
}
}
section(.class("card listing-form__section")) {
h2(.class("type-panel")) {
"3. Policies"
}
div(.class("listing-form__grid")) {
SelectField(
name: ShopForm.Field.returns.rawValue,
title: "Returns",
options: ShopForm.returns.map { ($0, $0) },
selected: form.value(.returns),
problem: problems[.returns]
)
SelectField(
name: ShopForm.Field.handling.rawValue,
title: "Handling time",
options: ShopForm.handlingDays.map { ("\($0)", $0 == 1 ? "1 business day" : "\($0) business days") },
selected: form.value(.handling),
problem: problems[.handling]
)
}
label(.class("check check--block")) {
input(
.type(.checkbox),
.name(ShopForm.Field.declaration.rawValue),
.value("1")
)
.attributes(.checked, when: form.value(.declaration) != nil)
span {
"I'll declare every defect on the checklist and photograph the actual copy. Undeclared defects are grounds for a problem report against my shop. "
a(.href(.Route.gradingGuide)) {
"Grading guide"
}
}
}
FieldNote(name: "declaration", hint: nil, problem: problems[.declaration])
p(.class("gate-note")) {
Icon(.info, size: 16)
span {
"Your shop is public from the moment you open it — anyone can browse it without an account. Shipping is priced per listing. A new shop starts unverified and unrated; ratings come only from finished sales."
}
}
}
div(.class("listing-form__submit")) {
a(
.class("button"),
.href(.Route.home)
) {
"Cancel"
}
button(
.class("button button--primary button--lg"),
.type(.submit)
) {
"Open shop"
}
}
}
}
}
}
@HTMLBuilder
var metadata: some HTML {
meta(
.name("robots"),
.content("noindex")
)
siteMetadata
}
var scripts: [any Asset] {
[StaticFile.shared]
}
var stylesheets: [any Asset] {
[StaticFile.shared, StaticFile.trade]
}
var title: String {
"Open a shop — CCN"
}
}
// MARK: - Helpers
private extension OpenShopPage {
/// A text field of the form, showing its value and its problem.
func field(
_ field: ShopForm.Field,
_ title: String,
placeholder: String,
hint: String? = nil,
isWide: Bool = false
) -> TextField {
.init(
name: field.rawValue,
title: title,
value: form.value(field) ?? "",
placeholder: placeholder,
hint: hint,
problem: problems[field],
isWide: isWide
)
}
/// A way of selling, as a radio card.
func kind(
_ kind: Shop.Kind,
_ title: String,
_ text: String
) -> some HTML {
label(.class("listing-form__mode")) {
input(
.type(.radio),
.name(ShopForm.Field.kind.rawValue),
.value(kind.rawValue)
)
.attributes(.checked, when: form.value(.kind) == kind.rawValue)
span {
strong {
title
}
span(.class("type-small")) {
text
}
}
}
}
}