From 72b9abf92f4fd0a4718ce67006d7a8b122aef818 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Fri, 4 Sep 2026 13:07:57 +0300 Subject: [PATCH 1/3] Feat: Updating Product Cards, Listing Pages and SearchController --- app/Catalog/ProductCard.php | 23 ++----- app/Catalog/ProductListingPage.php | 51 +++++++++----- app/Http/Controllers/SearchController.php | 83 +++-------------------- 3 files changed, 50 insertions(+), 107 deletions(-) diff --git a/app/Catalog/ProductCard.php b/app/Catalog/ProductCard.php index b558e5b..d0c00f2 100644 --- a/app/Catalog/ProductCard.php +++ b/app/Catalog/ProductCard.php @@ -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 $product one item from ProductService's localized array shape * @return array{name: ?string, price: ?string, image: ?string, href: string} diff --git a/app/Catalog/ProductListingPage.php b/app/Catalog/ProductListingPage.php index cd08a03..7eb3be1 100644 --- a/app/Catalog/ProductListingPage.php +++ b/app/Catalog/ProductListingPage.php @@ -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 $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 */ - 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(...)) diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 46fc875..1044520 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -2,20 +2,12 @@ namespace App\Http\Controllers; -use App\Catalog\ProductCard; -use App\Catalog\ProductSortOptions; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; -use Lunar\Models\Product; -use Modules\Core\Catalog\Enums\ProductSort; -use Modules\Core\Catalog\Services\ProductSearchService; +use App\Catalog\ProductListing; +use App\Catalog\ProductListingPage; class SearchController extends Controller { - private const PER_PAGE = 12; - - public function __construct(private readonly ProductSearchService $search) {} + public function __construct(private readonly ProductListingPage $listingPage) {} public function show(string $locale) { @@ -27,68 +19,15 @@ public function show(string $locale) return redirect()->route('products'); } - $sort = ProductSort::tryFrom((string) request()->query('sort')); - $page = max(1, (int) request()->query('page', 1)); + $listing = ProductListing::fromRequest(request()); - $cards = $this->cardsFor($query, $sort); - - $products = new LengthAwarePaginator( - items: $cards->forPage($page, self::PER_PAGE)->values(), - total: $cards->count(), - perPage: self::PER_PAGE, - currentPage: $page, - options: ['path' => LengthAwarePaginator::resolveCurrentPath()], - ); - $products->appends(array_filter( - ['q' => $query, 'sort' => $sort?->value], - fn ($value) => $value !== null, - )); - - // Sort dropdown links: same query, swapped sort (default sort => no param). - $sortOptions = ProductSortOptions::build( - $sort, - fn (?ProductSort $option) => route('search', array_filter( - ['q' => $query, 'sort' => $option?->value], - fn ($value) => $value !== null, - )), + $data = $this->listingPage->build( + $listing, + fn (array $overrides) => route('search', ['q' => $query] + $overrides), + collectionId: null, + query: $query, ); - return view('search.index', [ - 'query' => $query, - 'products' => $products, - 'sortOptions' => $sortOptions, - ]); + return view('search.index', [...$data, 'query' => $query]); } - - /** - * Full-text matches as product-card arrays. ProductSearchService returns - * hydrated models in relevance order with no sort or pagination, so ordering - * is done here in PHP and the controller paginates the mapped collection. - * - * @return Collection> - */ - private function cardsFor(string $query, ?ProductSort $sort): Collection - { - $results = $this->search->search($query)->load(['variants.prices', 'media']); - - return $this->sortResults($results, $sort) - ->map(ProductCard::fromModel(...)) - ->values(); - } - - /** - * @param EloquentCollection $results - * @return EloquentCollection - */ - private function sortResults(EloquentCollection $results, ?ProductSort $sort): EloquentCollection - { - $price = fn (Product $product) => $product->variants->first()?->prices->first()?->price->value ?? 0; - - return match ($sort) { - ProductSort::PriceAsc => $results->sortBy($price)->values(), - ProductSort::PriceDesc => $results->sortByDesc($price)->values(), - ProductSort::Newest => $results->sortByDesc('created_at')->values(), - default => $results, // Meilisearch relevance order - }; - } -} +} \ No newline at end of file From e57bfe3d420ec40273a23d68c6f729b22d2c7656 Mon Sep 17 00:00:00 2001 From: elvira Date: Fri, 4 Sep 2026 12:58:09 +0300 Subject: [PATCH 2/3] minor change in contact page --- composer.lock | 45 ++++++++++++++++--------------- resources/views/contact.blade.php | 2 +- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/composer.lock b/composer.lock index 9985868..3e4e99e 100644 --- a/composer.lock +++ b/composer.lock @@ -515,11 +515,11 @@ }, { "name": "boboko/core", - "version": "0.11.1", + "version": "0.13.1", "source": { "type": "git", "url": "https://code.radical-elements.com/boboko/core.git", - "reference": "026ab5bc2da9b61b57d86aaf08c8fbc38d80f590" + "reference": "e9aa08a3383f9dfa48564fd9b40d2193d5a1d2f3" }, "require": { "laravel/framework": "^12.0", @@ -556,7 +556,8 @@ "Modules\\Core\\Providers\\CatalogServiceProvider", "Modules\\Core\\Providers\\CartServiceProvider", "Modules\\Core\\Providers\\ReviewServiceProvider", - "Modules\\Core\\Providers\\ShippingServiceProvider" + "Modules\\Core\\Providers\\ShippingServiceProvider", + "Modules\\Core\\Providers\\OrderServiceProvider" ] } }, @@ -566,7 +567,7 @@ } }, "description": "Core module — authentication and shared panel behaviour", - "time": "2026-09-01T10:04:54+00:00" + "time": "2026-09-03T15:28:22+00:00" }, { "name": "brick/math", @@ -5099,16 +5100,16 @@ }, { "name": "monolog/monolog", - "version": "3.10.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + "reference": "147f303310f06334f03f409e49d7ad1e275ff05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/147f303310f06334f03f409e49d7ad1e275ff05a", + "reference": "147f303310f06334f03f409e49d7ad1e275ff05a", "shasum": "" }, "require": { @@ -5186,7 +5187,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + "source": "https://github.com/Seldaek/monolog/tree/3.11.0" }, "funding": [ { @@ -5198,7 +5199,7 @@ "type": "tidelift" } ], - "time": "2026-01-02T08:56:05+00:00" + "time": "2026-09-02T12:39:56+00:00" }, { "name": "nesbot/carbon", @@ -8791,16 +8792,16 @@ }, { "name": "spatie/laravel-medialibrary", - "version": "11.23.6", + "version": "11.23.7", "source": { "type": "git", "url": "https://github.com/spatie/laravel-medialibrary.git", - "reference": "5319032c4197354fe21889e6808b388f6fb257d9" + "reference": "94b11766ea5e10b1e2a88159b24937eae89a5d5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/5319032c4197354fe21889e6808b388f6fb257d9", - "reference": "5319032c4197354fe21889e6808b388f6fb257d9", + "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/94b11766ea5e10b1e2a88159b24937eae89a5d5f", + "reference": "94b11766ea5e10b1e2a88159b24937eae89a5d5f", "shasum": "" }, "require": { @@ -8885,7 +8886,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-medialibrary/issues", - "source": "https://github.com/spatie/laravel-medialibrary/tree/11.23.6" + "source": "https://github.com/spatie/laravel-medialibrary/tree/11.23.7" }, "funding": [ { @@ -8897,7 +8898,7 @@ "type": "github" } ], - "time": "2026-09-01T09:46:42+00:00" + "time": "2026-09-03T13:52:57+00:00" }, { "name": "spatie/laravel-package-tools", @@ -12654,16 +12655,16 @@ }, { "name": "technikermathe/blade-lucide-icons", - "version": "v3.173.0", + "version": "v3.174.0", "source": { "type": "git", "url": "https://github.com/PascaleBeier/blade-lucide-icons.git", - "reference": "5af6ffb7bf190d3c4a62768a6db14890aab69d24" + "reference": "e22d65587b2a6f21405161f399bda8c871135724" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/5af6ffb7bf190d3c4a62768a6db14890aab69d24", - "reference": "5af6ffb7bf190d3c4a62768a6db14890aab69d24", + "url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/e22d65587b2a6f21405161f399bda8c871135724", + "reference": "e22d65587b2a6f21405161f399bda8c871135724", "shasum": "" }, "require": { @@ -12713,9 +12714,9 @@ ], "support": { "issues": "https://github.com/PascaleBeier/blade-lucide-icons/issues", - "source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.173.0" + "source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.174.0" }, - "time": "2026-09-02T01:57:09+00:00" + "time": "2026-09-04T01:58:29+00:00" }, { "name": "thecodingmachine/safe", diff --git a/resources/views/contact.blade.php b/resources/views/contact.blade.php index a2e0e54..467e561 100644 --- a/resources/views/contact.blade.php +++ b/resources/views/contact.blade.php @@ -17,7 +17,7 @@ data-controller="appear" data-appear-visible-class="is-visible"> Επικοινώνησε
- μαζί μας άμεσα + μαζί μας άμεσα

From 9abe45c5f7327eb6d86eb639058874849f213237 Mon Sep 17 00:00:00 2001 From: elvira Date: Fri, 4 Sep 2026 13:19:24 +0300 Subject: [PATCH 3/3] minor changes in comments --- app/Catalog/ProductListingPage.php | 7 +------ app/Http/Controllers/SearchController.php | 4 +--- composer.lock | 6 +++--- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/Catalog/ProductListingPage.php b/app/Catalog/ProductListingPage.php index 7eb3be1..dcf322e 100644 --- a/app/Catalog/ProductListingPage.php +++ b/app/Catalog/ProductListingPage.php @@ -28,10 +28,6 @@ public function __construct( * @param Closure(array): 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 */ public function build(ProductListing $listing, Closure $url, ?int $collectionId = null, ?string $query = null): array @@ -65,8 +61,7 @@ public function build(ProductListing $listing, Closure $url, ?int $collectionId // Slider bounds — the price span of everything matching the *other* // filters, rounded to whole euros, plus whether the current price params - // actually narrow that span. All computed in core now - // (ProductService::priceSliderBounds()). + // actually narrow that span. All computed in core $priceBounds = $result->priceBounds; return [ diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 1044520..7288c10 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -13,8 +13,6 @@ public function show(string $locale) { $query = trim((string) request()->query('q', '')); - // Nothing to search for — send them to the all-products page rather than - // render an empty results page. if ($query === '') { return redirect()->route('products'); } @@ -30,4 +28,4 @@ public function show(string $locale) return view('search.index', [...$data, 'query' => $query]); } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 3e4e99e..970a0d0 100644 --- a/composer.lock +++ b/composer.lock @@ -515,11 +515,11 @@ }, { "name": "boboko/core", - "version": "0.13.1", + "version": "0.14.0", "source": { "type": "git", "url": "https://code.radical-elements.com/boboko/core.git", - "reference": "e9aa08a3383f9dfa48564fd9b40d2193d5a1d2f3" + "reference": "4ff9bdacc3394bb02537998a2d43adbc2799768a" }, "require": { "laravel/framework": "^12.0", @@ -567,7 +567,7 @@ } }, "description": "Core module — authentication and shared panel behaviour", - "time": "2026-09-03T15:28:22+00:00" + "time": "2026-09-04T10:06:26+00:00" }, { "name": "brick/math",