274 lines
11 KiB
Swift
274 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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|