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 } } } } }