Implemented phase 1 for the POC prototype in the Website service.
This commit is contained in:
1 parent
6d7ab4bcba
commit
ea89ea892a
37 files changed
+4173
-43
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import Hummingbird
|
||||
import HummingbirdElementary
|
||||
import Infrastructure
|
||||
import Persistence
|
||||
|
||||
/// Serves the public catalogue: the search results, a listing's detail, and a seller's shop.
|
||||
///
|
||||
/// Unlike the landing page, these screens depend on the request, so each is rendered per request rather than cached.
|
||||
///
|
||||
/// - Note: `Context` is the request context the routes are resolved against, and must match the context of the router the routes are added to.
|
||||
public struct CatalogueController<Context: RequestContext> {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The analytics tracker the pages embed, or `nil` to omit it.
|
||||
private let analytics: Analytics?
|
||||
|
||||
/// The version token appended to the pages' asset URLs, or `nil` to leave them unversioned.
|
||||
private let assetVersion: String?
|
||||
|
||||
/// The repository the shops and listings are read through.
|
||||
private let repository: CatalogueRepository
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a catalogue controller.
|
||||
/// - Parameters:
|
||||
/// - repository: the repository the shops and listings are read through.
|
||||
/// - assetVersion: the version token appended to the pages' asset URLs, or `nil` (the default) to leave them unversioned.
|
||||
/// - analytics: the analytics tracker the pages embed, or `nil` (the default) to omit it.
|
||||
public init(
|
||||
repository: CatalogueRepository,
|
||||
assetVersion: String? = nil,
|
||||
analytics: Analytics? = nil
|
||||
) {
|
||||
self.analytics = analytics
|
||||
self.assetVersion = assetVersion
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RouterController
|
||||
|
||||
extension CatalogueController: RouterController {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
public var routes: RouteCollection<Context> {
|
||||
let routes = RouteCollection(context: Context.self)
|
||||
|
||||
routes.get(
|
||||
.Catalogue.search,
|
||||
use: search
|
||||
)
|
||||
routes.get(
|
||||
.Catalogue.listing,
|
||||
use: listing
|
||||
)
|
||||
routes.get(
|
||||
.Catalogue.shop,
|
||||
use: shop
|
||||
)
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension CatalogueController {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// The number of other listings shown under a listing: one line of the desktop grid.
|
||||
static var relatedCount: Int { 5 }
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Handles a request for a listing's detail; an unknown slug is not found.
|
||||
@Sendable
|
||||
func listing(
|
||||
request: Request,
|
||||
context: Context
|
||||
) async throws -> HTMLResponse {
|
||||
guard let listing = try await repository.listing(slug: context.parameters.require(.slug)) else {
|
||||
throw HTTPError(.notFound)
|
||||
}
|
||||
|
||||
let related = try await repository
|
||||
.listings(matching: .init(shop: listing.shop.slug))
|
||||
.filter { $0.slug != listing.slug }
|
||||
.prefix(Self.relatedCount)
|
||||
|
||||
return .init { [analytics, assetVersion] in
|
||||
ListingPage(
|
||||
listing: listing,
|
||||
related: Array(related),
|
||||
assetVersion: assetVersion,
|
||||
analytics: analytics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a request for the search results, filtered through the query parameters.
|
||||
@Sendable
|
||||
func search(
|
||||
request: Request,
|
||||
context: Context
|
||||
) async throws -> HTMLResponse {
|
||||
let parameters = SearchParameters(query: request.uri.query)
|
||||
let matches = try await repository.listings(matching: parameters.filter)
|
||||
let publishers = try await repository.publishers()
|
||||
|
||||
return .init { [analytics, assetVersion] in
|
||||
SearchPage(
|
||||
parameters: parameters,
|
||||
matches: matches,
|
||||
publishers: publishers,
|
||||
assetVersion: assetVersion,
|
||||
analytics: analytics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a request for a seller's shop; an unknown slug is not found.
|
||||
@Sendable
|
||||
func shop(
|
||||
request: Request,
|
||||
context: Context
|
||||
) async throws -> HTMLResponse {
|
||||
guard let shop = try await repository.shop(slug: context.parameters.require(.slug)) else {
|
||||
throw HTTPError(.notFound)
|
||||
}
|
||||
|
||||
let parameters = SearchParameters(query: request.uri.query)
|
||||
let inventory = try await repository.listings(matching: .init(shop: shop.slug))
|
||||
let matches = try await repository.listings(matching: parameters.filter(inShop: shop.slug))
|
||||
|
||||
return .init { [analytics, assetVersion] in
|
||||
ShopPage(
|
||||
shop: shop,
|
||||
inventory: inventory,
|
||||
matches: matches,
|
||||
parameters: parameters,
|
||||
assetVersion: assetVersion,
|
||||
analytics: analytics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension RouterPath {
|
||||
/// A namespace for the ``CatalogueController`` route paths.
|
||||
enum Catalogue {
|
||||
/// The path of a listing's detail.
|
||||
static let listing: RouterPath = .init(.Route.listing(":\(String.slug)"))
|
||||
/// The path of the search results.
|
||||
static let search: RouterPath = .init(.Route.search)
|
||||
/// The path of a seller's shop.
|
||||
static let shop: RouterPath = .init(.Route.shop(":\(String.slug)"))
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
/// The name of the route parameter carrying a listing's or a shop's slug.
|
||||
static let slug = "slug"
|
||||
}
|
||||
@@ -31,10 +31,12 @@ public struct RootController<Context: RequestContext> {
|
||||
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
|
||||
/// - siteOrigin: the public origin the page derives its canonical URL and language alternates from, or `nil` (the default) to omit them.
|
||||
/// - analytics: the analytics tracker the landing page embeds, or `nil` (the default) to omit it.
|
||||
/// - listings: the listings the landing page shows in its rows; none by default.
|
||||
public init(
|
||||
assetVersion: String? = nil,
|
||||
siteOrigin: String? = nil,
|
||||
analytics: Analytics? = nil
|
||||
analytics: Analytics? = nil,
|
||||
listings: HomeListings = .init()
|
||||
) {
|
||||
self.negotiate = .init(bundle: .module)
|
||||
// The bare route negotiates, so the pages declare `Vary: Accept-Language`; the prefixed editions share the cache
|
||||
@@ -47,7 +49,8 @@ public struct RootController<Context: RequestContext> {
|
||||
locale: $0,
|
||||
assetVersion: assetVersion,
|
||||
siteOrigin: siteOrigin,
|
||||
analytics: analytics
|
||||
analytics: analytics,
|
||||
listings: listings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user