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

237 lines
9.0 KiB
Swift

import Elementary
import Foundation
import Infrastructure
import Persistence
/// The account: the display name, the passkeys that sign it in, and the way out.
struct AccountPage {
// MARK: Properties
let analytics: Analytics?
let assetVersion: String?
let locale: Locale
/// The moment stamps are read relative to.
let now: Date
/// The account's passkeys, the oldest first, with when each was created.
let passkeys: [(passkey: Passkey, createdAt: Date)]
/// What is wrong with the display name posted, or `nil` when nothing is.
let problem: String?
/// The account shown.
let user: User
/// The signed-in viewer.
let viewer: Viewer?
// MARK: Initializers
/// Creates the account screen.
/// - Parameters:
/// - user: the account shown.
/// - passkeys: the account's passkeys, the oldest first, with when each was created.
/// - problem: what is wrong with the display name posted, or `nil` (the default) when nothing is.
/// - viewer: the signed-in viewer.
/// - now: the moment stamps are read relative to; the present by default.
/// - 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(
user: User,
passkeys: [(passkey: Passkey, createdAt: Date)],
problem: String? = nil,
viewer: Viewer? = nil,
now: Date = .init(),
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.locale = .init(identifier: "en")
self.now = now
self.passkeys = passkeys
self.problem = problem
self.user = user
self.viewer = viewer
}
}
// MARK: - Page
extension AccountPage: Page {
// MARK: Computed
var content: some HTML {
SiteLayout(viewer: viewer) {
div(.class("page account")) {
aside(.class("account__rail")) {
p(.class("type-eyebrow")) {
"Account"
}
h1(.class("type-panel")) {
user.displayName
}
nav(
.class("account__nav"),
.custom(name: "aria-label", value: "Account sections")
) {
a(.href("#profile")) {
"Profile"
}
a(.href("#passkeys")) {
"Passkeys"
}
a(.href(.Route.wants)) {
"Want list"
}
a(.href(.Route.messages)) {
"Messages"
}
a(.href(viewer?.hasShop == true ? .Route.sellerDashboard : .Route.openShop)) {
viewer?.hasShop == true ? "My shop" : "Open a shop"
}
}
}
div(.class("account__main")) {
section(
.class("card account__section"),
.id("profile")
) {
h2(.class("type-panel")) {
"Profile"
}
form(
.class("account__form"),
.action("\(String.Route.account)/name"),
.method(.post)
) {
TextField(
name: "name",
title: "Display name",
value: user.displayName,
hint: "How sellers and buyers see you. Signed in as \(user.email).",
problem: problem,
maxLength: 40
)
button(
.class("button button--dark"),
.type(.submit)
) {
"Save"
}
}
}
section(
.class("card account__section"),
.id("passkeys"),
.custom(name: "data-passkey-form", value: "")
) {
div(.class("section-head")) {
h2(.class("type-panel")) {
"Passkeys"
}
// Shown by `js/passkey.js` once the browser is known to do WebAuthn.
button(
.class("button button--primary button--sm"),
.type(.button),
.hidden,
.custom(name: "data-passkey", value: "add")
) {
Icon(.key, size: 15)
"Add a passkey"
}
}
p(.class("type-body-sm account__text")) {
"Any of these signs you in. Keep at least two on different devices so a lost phone never locks you out. Revoking one takes effect immediately."
}
p(
.class("field__error"),
.custom(name: "role", value: "alert"),
.custom(name: "data-passkey-status", value: "")
) {}
if passkeys.isEmpty {
p(.class("notice")) {
"No passkey yet: this account signs in through the prototype's demo sign-in."
}
} else {
ul(.class("rows")) {
ForEach(Array(passkeys.enumerated())) { index, entry in
li(.class("rows__row rows__row--passkey")) {
Icon(.key)
span {
strong {
"Passkey \(index + 1)"
}
span(.class("type-small")) {
" · created \(entry.createdAt.stamp(relativeTo: now))"
}
}
form(
.action("\(String.Route.account)/passkeys/revoke"),
.method(.post)
) {
input(
.type(.hidden),
.name("credential"),
.value(entry.passkey.credentialID)
)
button(
.class("button button--sm"),
.type(.submit)
) {
"Revoke"
}
}
}
}
}
}
}
section(.class("card account__section")) {
h2(.class("type-panel")) {
"Sign out"
}
form(
.action(.Route.signOut),
.method(.post)
) {
button(
.class("button button--outline"),
.type(.submit)
) {
"Sign out of this device"
}
}
}
}
}
}
}
@HTMLBuilder
var metadata: some HTML {
meta(
.name("robots"),
.content("noindex")
)
siteMetadata
}
var scripts: [any Asset] {
[StaticFile.shared, StaticFile.passkey]
}
var stylesheets: [any Asset] {
[StaticFile.shared, StaticFile.trade]
}
var title: String {
"Account — CCN"
}
}