Files
ccn/Services/Website/Sources/Library/Internal/Components/SiteHeader.swift
T

261 lines
7.1 KiB
Swift

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: viewer.newWantMatches == 1 ? "new match" : "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-tablet"),
.href(viewer.hasShop ? .Route.sellerDashboard : .Route.openShop)
) {
viewer.hasShop ? "My shop" : "Open a 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
}
}
}
/// 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)"
}
}
}
}
}