import Elementary /// The site header: the wordmark, the format navigation, the search field, and the viewer's actions. /// /// One markup serves all three layouts; the shared stylesheet rearranges it through grid areas. struct SiteHeader: HTML { // MARK: Properties /// The format marked as current in the navigation, or `nil` for none. let active: Format? /// The text the search field is filled with, or `nil` to leave it empty. let query: String? /// The signed-in viewer, or `nil` to render the signed-out actions. let viewer: Viewer? // MARK: Initializers /// Creates a site header. /// - Parameters: /// - active: the format marked as current, or `nil` (the default) for none. /// - query: the text the search field is filled with, or `nil` (the default) to leave it empty. /// - viewer: the signed-in viewer, or `nil` (the default) to render the signed-out actions. init( active: Format? = nil, query: String? = nil, viewer: Viewer? = nil ) { self.active = active self.query = query self.viewer = viewer } // MARK: Computed var body: some HTML { header(.class("site-header")) { div(.class("site-header__inner")) { a( .class("site-header__brand"), .href(.Route.home) ) { "CCN" } navigation search div(.class("site-header__actions")) { if let viewer { actions(for: viewer) } else { guestActions } } } } } } // MARK: - Helpers private extension SiteHeader { // MARK: Computed /// The signed-out actions: *Open a shop* and the passkey sign-in. @HTMLBuilder var guestActions: some HTML { a( .class("site-header__icon-link phone-only"), .href(.Route.openShop), .custom( name: "aria-label", value: "Open a shop" ) ) { Icon(.shop, size: 22) } a( .class("site-header__text-link from-desktop"), .href(.Route.openShop) ) { "Open a shop" } a( .class("site-header__sign-in"), .href(.Route.signIn) ) { Icon(.key, size: 15) span { "Sign in" span(.class("from-desktop")) { " with passkey" } } } } /// The format links, plus the series and events links shown on tablet only. var navigation: some HTML { nav( .class("site-nav"), .custom( name: "aria-label", value: "Formats" ) ) { for format in Format.allCases { if format == active { a( .class("site-nav__link"), .href(format.path), .custom( name: "aria-current", value: "page" ) ) { format.label } } else { a( .class("site-nav__link"), .href(format.path) ) { format.label } } } a( .class("site-nav__link site-nav__link--extra"), .href(.Route.series) ) { "Series" } a( .class("site-nav__link site-nav__link--extra"), .href(.Route.events) ) { "Events" } } } /// The search field, a plain `GET` form. var search: some HTML { form( .class("site-search"), .action(.Route.search), .method(.get), .custom( name: "role", value: "search" ) ) { label( .class("visually-hidden"), .for("site-search-query") ) { "Search listings" } input( .class("site-search__input"), .id("site-search-query"), .type(.search), .name("q"), .value(query ?? ""), .placeholder("Search title, issue, grade or shop") ) button( .class("site-search__submit"), .type(.submit), .custom( name: "aria-label", value: "Search" ) ) { Icon(.search, size: 16) } } } // MARK: Methods /// The signed-in actions: want list, messages, shop, and account. /// - Parameter viewer: the signed-in viewer. @HTMLBuilder func actions( for viewer: Viewer ) -> some HTML { counter( .bell, label: "Want list", count: viewer.newWantMatches, unit: "new matches", path: .Route.wants ) counter( .message, label: "Messages", count: viewer.unreadMessages, unit: "unread", path: .Route.messages ) a( .class("site-header__text-link site-header__text-link--strong from-desktop"), .href(.Route.sellerDashboard) ) { "My shop" } a( .class("site-header__account"), .href(.Route.account), .custom( name: "aria-label", value: "Account of \(viewer.displayName)" ) ) { span(.class("from-desktop")) { viewer.firstName } span(.class("site-header__avatar")) { 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. /// - Parameters: /// - glyph: the glyph the link shows. /// - label: the link's accessible name. /// - count: the number the badge shows. /// - unit: what the count measures. /// - path: the path the link leads to. func counter( _ glyph: Icon.Glyph, label: String, count: Int, unit: String, path: String ) -> some HTML { a( .class("site-header__icon-link"), .href(path), .custom( name: "aria-label", value: count > 0 ? "\(label), \(count) \(unit)" : label ) ) { Icon(glyph) if count > 0 { span(.class("site-header__badge")) { "\(count)" } } } } }