generated from boboko/starter
Feat: Updating Product Cards, Listing Pages and SearchController
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Catalog;
|
||||
|
||||
use Lunar\Models\Product;
|
||||
|
||||
/**
|
||||
* Presentation shaping — how a storefront product listing/grid card is built:
|
||||
* name, price, image, href. Deliberately not in boboko-core: `href` depends on
|
||||
@@ -12,26 +10,13 @@
|
||||
* CategoryController / ProductController / SearchController don't each
|
||||
* hand-write the same name/price/image/href mapping.
|
||||
*
|
||||
* Two sources, because the storefront reads products both ways: a hydrated
|
||||
* Eloquent model (full-text search via ProductSearchService), or a localized
|
||||
* index array (ProductService::list()/getById()/random()). Model callers must
|
||||
* eager-load `variants.prices` and `media`.
|
||||
* One source: a localized index array — ProductService::list()/getById()/
|
||||
* random(), and now ProductSearchService::search() too, all return the exact
|
||||
* same document shape (see ProductListingResult), so this is the only mapping
|
||||
* every storefront listing page needs.
|
||||
*/
|
||||
final class ProductCard
|
||||
{
|
||||
/**
|
||||
* @return array{name: ?string, price: ?string, image: ?string, href: string}
|
||||
*/
|
||||
public static function fromModel(Product $product): array
|
||||
{
|
||||
return [
|
||||
'name' => $product->translateAttribute('name'), //@todo check this
|
||||
'price' => $product->variants->first()?->prices->first()?->price->decimal, //@todo check this
|
||||
'image' => $product->media->first()?->getUrl(),
|
||||
'href' => route('product.show', ['id' => $product->id]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $product one item from ProductService's localized array shape
|
||||
* @return array{name: ?string, price: ?string, image: ?string, href: string}
|
||||
|
||||
@@ -4,41 +4,60 @@
|
||||
|
||||
use Closure;
|
||||
use Modules\Core\Catalog\Enums\ProductSort;
|
||||
use Modules\Core\Catalog\Services\ProductSearchService;
|
||||
use Modules\Core\Catalog\Services\ProductService;
|
||||
|
||||
/**
|
||||
* Assembles the data the shared shop listing body (shop/partials/listing.blade.php)
|
||||
* needs — the product page, price-slider bounds, sort links and the "clear price"
|
||||
* link. Used by both the category page (scoped to a collection) and the
|
||||
* all-products page; the `$url` closure turns a query-param array into a URL for
|
||||
* whichever page is calling, so this class never has to know the route.
|
||||
* link. Used by the category page (scoped to a collection), the all-products
|
||||
* page, and the search page (scoped to a query — see $query below); the `$url`
|
||||
* closure turns a query-param array into a URL for whichever page is calling,
|
||||
* so this class never has to know the route.
|
||||
*/
|
||||
final class ProductListingPage
|
||||
{
|
||||
private const PER_PAGE = 12;
|
||||
|
||||
public function __construct(private readonly ProductService $products) {}
|
||||
public function __construct(
|
||||
private readonly ProductService $products,
|
||||
private readonly ProductSearchService $search,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Closure(array<string, string|int>): string $url
|
||||
* @param ?int $collectionId scope to a collection, or null for every product
|
||||
* @param ?string $query scope to a text search — when given, calls
|
||||
* ProductSearchService::search() instead of ProductService::list().
|
||||
* ProductSearchService::search() returns the exact same
|
||||
* Modules\Core\Catalog\DTOs\ProductListingResult list() does, so
|
||||
* everything below this point is identical either way.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function build(ProductListing $listing, Closure $url, ?int $collectionId = null): array
|
||||
public function build(ProductListing $listing, Closure $url, ?int $collectionId = null, ?string $query = null): array
|
||||
{
|
||||
$filters = $listing->filters($collectionId);
|
||||
|
||||
// Listing reads from the Meilisearch index via ProductService, not
|
||||
// Eloquent. list() returns a ProductListingResult — the product page
|
||||
// plus the price-slider bounds from one call; the controller no longer
|
||||
// stitches list() + priceRange() together itself. Sort/filter/page all
|
||||
// come from $listing (the query string).
|
||||
$result = $this->products->list(
|
||||
filters: $filters,
|
||||
perPage: self::PER_PAGE,
|
||||
page: $listing->page,
|
||||
sort: $listing->sort,
|
||||
);
|
||||
// Listing reads from the Meilisearch index via ProductService/
|
||||
// ProductSearchService, not Eloquent. Both return a
|
||||
// ProductListingResult — the product page plus the price-slider
|
||||
// bounds (and available tags) from one call; the controller no
|
||||
// longer stitches list()/search() + priceRange() together itself.
|
||||
// Sort/filter/page all come from $listing (the query string).
|
||||
$result = $query !== null
|
||||
? $this->search->search(
|
||||
query: $query,
|
||||
filters: $filters,
|
||||
sort: $listing->sort,
|
||||
perPage: self::PER_PAGE,
|
||||
page: $listing->page,
|
||||
)
|
||||
: $this->products->list(
|
||||
filters: $filters,
|
||||
perPage: self::PER_PAGE,
|
||||
page: $listing->page,
|
||||
sort: $listing->sort,
|
||||
);
|
||||
|
||||
$products = $result->products
|
||||
->through(ProductCard::fromIndexed(...))
|
||||
|
||||
Reference in New Issue
Block a user