208 lines
7.2 KiB
Swift
208 lines
7.2 KiB
Swift
import Foundation
|
|
|
|
extension String {
|
|
/// A namespace for the paths of the site's screens.
|
|
///
|
|
/// Single-sourced, so the navigation and the controllers never spell a path differently.
|
|
enum Route {
|
|
/// The account screen.
|
|
static let account = "/account"
|
|
/// The collection tracker.
|
|
static let collection = "/collection"
|
|
/// The side-by-side comparison of copies.
|
|
static let compare = "/compare"
|
|
/// The events where shops trade in person.
|
|
static let events = "/events"
|
|
/// The public grading guide.
|
|
static let gradingGuide = "/grading-guide"
|
|
/// The browse / home screen.
|
|
static let home = "/"
|
|
/// The message threads.
|
|
static let messages = "/messages"
|
|
/// The open-a-shop onboarding.
|
|
static let openShop = "/sell/open-a-shop"
|
|
/// The passkey recovery screen.
|
|
static let recover = "/recover"
|
|
/// The search results screen.
|
|
static let search = "/search"
|
|
/// The seller dashboard.
|
|
static let sellerDashboard = "/sell"
|
|
/// The series index.
|
|
static let series = "/series"
|
|
/// The passkey sign-in screen.
|
|
static let signIn = "/sign-in"
|
|
/// The path a sign-out is posted to.
|
|
static let signOut = "/sign-out"
|
|
/// The want list.
|
|
static let wants = "/wants"
|
|
/// The first-sign-in screen.
|
|
static let welcome = "/welcome"
|
|
|
|
/// The path an offer is answered at.
|
|
/// - Parameter id: the offer's identifier.
|
|
static func answer(
|
|
offer id: UUID
|
|
) -> String {
|
|
"\(sellerDashboard)/offers/\(id.uuidString.lowercased())"
|
|
}
|
|
|
|
/// The path buying a listing outright starts at.
|
|
/// - Parameter slug: the listing's URL slug.
|
|
static func buy(
|
|
_ slug: String
|
|
) -> String {
|
|
"\(listing(slug))/buy"
|
|
}
|
|
|
|
/// The path of a conversation.
|
|
/// - Parameter id: the conversation's identifier.
|
|
static func conversation(
|
|
_ id: UUID
|
|
) -> String {
|
|
"\(messages)/\(id.uuidString.lowercased())"
|
|
}
|
|
|
|
/// The path a gated action leads to: the action itself when signed in, else the sign-in screen, returning to where the action was taken.
|
|
/// - Parameters:
|
|
/// - path: the action's own path.
|
|
/// - origin: the path of the screen the action is taken on.
|
|
/// - viewer: the signed-in viewer, or `nil` when signed out.
|
|
static func gated(
|
|
_ path: String,
|
|
from origin: String,
|
|
viewer: Viewer?
|
|
) -> String {
|
|
viewer == nil ? signIn(returningTo: origin) : path
|
|
}
|
|
|
|
/// The path of a listing's detail screen.
|
|
/// - Parameter slug: the listing's URL slug.
|
|
static func listing(
|
|
_ slug: String
|
|
) -> String {
|
|
"/listing/\(slug)"
|
|
}
|
|
|
|
/// The path of a new message thread about a listing.
|
|
/// - Parameter slug: the listing's URL slug.
|
|
static func message(
|
|
listing slug: String
|
|
) -> String {
|
|
"\(messages)/new?listing=\(slug)"
|
|
}
|
|
|
|
/// The path of a new message thread with a shop.
|
|
/// - Parameter slug: the shop's URL slug.
|
|
static func message(
|
|
shop slug: String
|
|
) -> String {
|
|
"\(messages)/new?shop=\(slug)"
|
|
}
|
|
|
|
/// The path of the offer sheet for a listing.
|
|
/// - Parameter slug: the listing's URL slug.
|
|
static func offer(
|
|
_ slug: String
|
|
) -> String {
|
|
"\(listing(slug))/offer"
|
|
}
|
|
|
|
/// The path of the search narrowed to the copies that can be picked up at an event.
|
|
/// - Parameter slug: the event's URL slug.
|
|
static func pickup(
|
|
at slug: String
|
|
) -> String {
|
|
"\(search)?pickup=\(slug)&ships-from=anywhere"
|
|
}
|
|
|
|
/// The path a one-time recovery link leads to.
|
|
static func recover(
|
|
token: String
|
|
) -> String {
|
|
"\(recover)/\(token)"
|
|
}
|
|
|
|
/// The path of a sale record.
|
|
/// - Parameter number: the record's number.
|
|
static func sale(
|
|
_ number: Int
|
|
) -> String {
|
|
"/sales/\(number)"
|
|
}
|
|
|
|
/// The path of a series' screen, optionally on the copies of one issue.
|
|
/// - Parameters:
|
|
/// - slug: the series' URL slug.
|
|
/// - issue: the issue whose copies are listed, or `nil` (the default) for none.
|
|
static func series(
|
|
_ slug: String,
|
|
issue: Int? = nil
|
|
) -> String {
|
|
"\(series)/\(slug)\(issue.map { "?issue=\($0)" } ?? "")"
|
|
}
|
|
|
|
/// The path of a shop's public screen.
|
|
/// - Parameter slug: the shop's URL slug.
|
|
static func shop(
|
|
_ slug: String
|
|
) -> String {
|
|
"/shop/\(slug)"
|
|
}
|
|
|
|
/// The path to return to after signing in: the one asked for when it stays on this site, else the home screen.
|
|
///
|
|
/// Only a rooted path is followed. Anything else — another origin, a scheme-relative `//host`, a backslash a browser reads as a slash —
|
|
/// would turn the sign-in into an open redirect.
|
|
/// - Parameter path: the path asked for, as the `return` parameter carried it.
|
|
static func safeReturn(
|
|
_ path: String?
|
|
) -> String {
|
|
guard
|
|
let path,
|
|
path.hasPrefix("/"),
|
|
!path.hasPrefix("//"),
|
|
!path.contains("\\"),
|
|
!path.unicodeScalars.contains(where: { $0.properties.generalCategory == .control })
|
|
else {
|
|
return home
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
/// The path of the first-sign-in screen, continuing to the given path afterwards.
|
|
static func welcome(
|
|
returningTo path: String
|
|
) -> String {
|
|
"\(welcome)?return=\(path.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "")"
|
|
}
|
|
|
|
/// The path of the form adding a want, filled from a listing or with a title.
|
|
static func newWant(
|
|
listing slug: String? = nil,
|
|
title: String? = nil
|
|
) -> String {
|
|
let query = [slug.map { "listing=\($0)" }, title.map { "title=\($0.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "")" }]
|
|
.compactMap(\.self)
|
|
.joined(separator: "&")
|
|
|
|
return "\(wants)/new\(query.isEmpty ? "" : "?\(query)")"
|
|
}
|
|
|
|
/// The path of the sign-in screen, returning to the given path afterwards.
|
|
/// - Parameter path: the path the visitor's gated action started from.
|
|
static func signIn(
|
|
returningTo path: String
|
|
) -> String {
|
|
"\(signIn)?return=\(path.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? "")"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Constants
|
|
|
|
private extension CharacterSet {
|
|
/// The characters a query value carries unescaped.
|
|
static let urlQueryValueAllowed = CharacterSet.urlQueryAllowed.subtracting(.init(charactersIn: "&=+?#"))
|
|
}
|