generated from boboko/starter
Feat: Updating Home, Product and Category Controllers and veiws to utilize the product service
This commit is contained in:
+5
-7
@@ -119,13 +119,11 @@ COPY docker/php/php.dev.ini /etc/php/8.5/cli/conf.d/99-app.ini
|
|||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
COPY composer.json composer.lock ./
|
# No build-time `composer install` here: composer.json's boboko/core path repo
|
||||||
RUN composer install \
|
# (../boboko-core) isn't visible in the build context, only once bind-mounted at
|
||||||
--no-interaction \
|
# container start — entrypoint.sh already runs composer install +
|
||||||
--no-scripts \
|
# composer update boboko/* on every boot, so this would be redundant even if it
|
||||||
--prefer-dist \
|
# could work.
|
||||||
--ignore-platform-reqs
|
|
||||||
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
COPY docker/entrypoint-worker.sh /entrypoint-worker.sh
|
COPY docker/entrypoint-worker.sh /entrypoint-worker.sh
|
||||||
RUN chmod +x /entrypoint.sh /entrypoint-worker.sh
|
RUN chmod +x /entrypoint.sh /entrypoint-worker.sh
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Lunar\Models\Collection;
|
use Lunar\Models\Collection;
|
||||||
use Modules\Core\Catalog\ProductFilters;
|
use Modules\Core\Catalog\ProductFilters;
|
||||||
use Modules\Core\Catalog\ProductService;
|
use Modules\Core\Catalog\ProductService;
|
||||||
@@ -19,29 +18,19 @@ public function show(string $locale, Collection $collection)
|
|||||||
$page = (int) request('page', 1);
|
$page = (int) request('page', 1);
|
||||||
|
|
||||||
// Listing/filtering reads from the Meilisearch index via ProductService,
|
// Listing/filtering reads from the Meilisearch index via ProductService,
|
||||||
// not Eloquent — see Modules\Core\Catalog\ProductService. It returns plain
|
// not Eloquent — see Modules\Core\Catalog\ProductService. list() returns a
|
||||||
// arrays (already localized/flattened), not Product models.
|
// real LengthAwarePaginator of plain arrays (already localized/flattened),
|
||||||
$result = $this->products->list(
|
// not Product models.
|
||||||
|
$products = $this->products->list(
|
||||||
filters: new ProductFilters(collectionId: $collection->id),
|
filters: new ProductFilters(collectionId: $collection->id),
|
||||||
perPage: $perPage,
|
perPage: $perPage,
|
||||||
page: $page,
|
page: $page,
|
||||||
);
|
)->through(fn (array $product) => [
|
||||||
|
'name' => $product['name'],
|
||||||
$products = new LengthAwarePaginator(
|
'price' => $product['price'],
|
||||||
items: collect($result['data'])->map(fn (array $product) => [
|
'image' => $product['media'][0]['url'] ?? null,
|
||||||
'name' => $product['name'],
|
'href' => route('product.show', ['id' => $product['id']]),
|
||||||
'price' => $product['price'],
|
]);
|
||||||
'image' => $product['media'][0]['url'] ?? null,
|
|
||||||
'href' => route('product.show', ['product' => $product['id']]),
|
|
||||||
]),
|
|
||||||
total: $result['meta']['total'],
|
|
||||||
perPage: $result['meta']['per_page'],
|
|
||||||
currentPage: $result['meta']['current_page'],
|
|
||||||
options: [
|
|
||||||
'path' => request()->url(),
|
|
||||||
'query' => request()->query(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return view('category.show', [
|
return view('category.show', [
|
||||||
'collection' => $collection,
|
'collection' => $collection,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public function index(string $locale)
|
|||||||
'name' => $product->translateAttribute('name'),
|
'name' => $product->translateAttribute('name'),
|
||||||
'price' => $product->variants->first()?->prices->first()?->price->decimal,
|
'price' => $product->variants->first()?->prices->first()?->price->decimal,
|
||||||
'image' => $product->media->first()?->getUrl(),
|
'image' => $product->media->first()?->getUrl(),
|
||||||
'href' => route('product.show', ['product' => $product]),
|
'href' => route('product.show', ['id' => $product->id]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return view('home', [
|
return view('home', [
|
||||||
|
|||||||
@@ -2,44 +2,44 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Lunar\Models\Product;
|
use Illuminate\Http\Response;
|
||||||
|
use Lunar\Models\Collection;
|
||||||
|
use Modules\Core\Catalog\ProductService;
|
||||||
|
|
||||||
class ProductController extends Controller
|
class ProductController extends Controller
|
||||||
{
|
{
|
||||||
public function show(string $locale, Product $product)
|
public function __construct(private readonly ProductService $products) {}
|
||||||
|
|
||||||
|
public function show(string $locale, int $id)
|
||||||
{
|
{
|
||||||
$product->load([
|
$product = $this->products->getById($id);
|
||||||
"variants.prices.currency",
|
|
||||||
"variants.values.option",
|
|
||||||
"media",
|
|
||||||
"collections",
|
|
||||||
]);
|
|
||||||
|
|
||||||
$option = $product->variants->first()?->values->first()?->option;
|
abort_if($product === null, Response::HTTP_NOT_FOUND);
|
||||||
|
|
||||||
$variantsData = $product->variants
|
$collection = $product['collections'][0] ?? null;
|
||||||
->map(
|
$collectionModel = $collection !== null ? Collection::find($collection) : null;
|
||||||
fn($v) => [
|
|
||||||
"id" => $v->id,
|
$variantsData = collect($product['variants'])
|
||||||
"price" => $v->prices->first()?->price->decimal,
|
->map(fn (array $variant) => [
|
||||||
"image" => null, // variant-level media not differentiated yet
|
'id' => $variant['id'],
|
||||||
],
|
'price' => $variant['prices'][0]['price'] ?? null,
|
||||||
)
|
'image' => $variant['media'][0]['url'] ?? null,
|
||||||
|
])
|
||||||
->values()
|
->values()
|
||||||
->toArray();
|
->all();
|
||||||
|
|
||||||
$firstImage = $product->media->first()?->getUrl();
|
$firstVariant = $product['variants'][0] ?? null;
|
||||||
|
$option = $firstVariant['options'][0]['option'] ?? null;
|
||||||
|
|
||||||
// temp categories here
|
// temp categories here
|
||||||
$categories = \Lunar\Models\Collection::orderBy("_lft")->get();
|
$categories = Collection::orderBy('_lft')->get();
|
||||||
|
|
||||||
// dd($product);
|
return view('product.show', [
|
||||||
|
'categories' => $categories,
|
||||||
return view("product.show", [
|
'collection' => $collectionModel,
|
||||||
"categories" => $categories,
|
'product' => $product,
|
||||||
"product" => $product,
|
'option' => $option,
|
||||||
"option" => $option,
|
'variantsData' => $variantsData,
|
||||||
"variantsData" => $variantsData,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
|
{{-- $variants: array of Modules\Core\Search\ProductIndexer's mapVariant() shape
|
||||||
|
(id, options: [{option, value, meta}], ...) — plain arrays, not Eloquent
|
||||||
|
models, since this is fed from Modules\Core\Catalog\ProductService. --}}
|
||||||
@props(['variants', 'option' => null])
|
@props(['variants', 'option' => null])
|
||||||
|
|
||||||
<div {{ $attributes }}>
|
<div {{ $attributes }}>
|
||||||
@if($option)
|
@if($option)
|
||||||
<p class="font-bold mb-3 text-sm uppercase tracking-wide">
|
<p class="font-bold mb-3 text-sm uppercase tracking-wide">
|
||||||
{{ $option->translate('name') }}: <span data-product-form-target="colorName" class="font-normal normal-case"></span>
|
{{ $option }}: <span data-product-form-target="colorName" class="font-normal normal-case"></span>
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap gap-2"
|
class="flex flex-wrap gap-2"
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="{{ $option?->translate('name') ?? 'Color' }}"
|
aria-label="{{ $option ?? 'Color' }}"
|
||||||
>
|
>
|
||||||
@foreach($variants as $variant)
|
@foreach($variants as $variant)
|
||||||
@php
|
@php
|
||||||
$value = $variant->values->first();
|
$value = $variant['options'][0] ?? null;
|
||||||
$label = $value?->translate('name') ?? '';
|
$label = $value['value'] ?? '';
|
||||||
$bg = $value?->meta['hex'] ?? '#cccccc';
|
$bg = $value['meta']['hex'] ?? '#cccccc';
|
||||||
@endphp
|
@endphp
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="color-swatch"
|
class="color-swatch"
|
||||||
data-product-form-target="swatch"
|
data-product-form-target="swatch"
|
||||||
data-action="click->product-form#selectVariant"
|
data-action="click->product-form#selectVariant"
|
||||||
data-variant-id="{{ $variant->id }}"
|
data-variant-id="{{ $variant['id'] }}"
|
||||||
style="background-color: {{ $bg }};"
|
style="background-color: {{ $bg }};"
|
||||||
aria-label="{{ $label }}"
|
aria-label="{{ $label }}"
|
||||||
aria-pressed="false"
|
aria-pressed="false"
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('title', $product->translateAttribute('name') . ' — ' . config('app.name'))
|
@section('title', $product['name'] . ' — ' . config('app.name'))
|
||||||
@section('description', $product->translateAttribute('description'))
|
@section('description', $product['description'])
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-8 py-12">
|
<div class="max-w-7xl mx-auto px-4 sm:px-8 py-12">
|
||||||
|
|
||||||
@php $collection = $product->collections->first(); @endphp
|
|
||||||
|
|
||||||
<x-breadcrumb class="mb-14 justify-end" :items="[
|
<x-breadcrumb class="mb-14 justify-end" :items="[
|
||||||
$collection
|
$collection
|
||||||
? ['label' => $collection->translateAttribute('name'), 'href' => route('category.show', ['collection' => $collection])]
|
? ['label' => $collection->translateAttribute('name'), 'href' => route('category.show', ['collection' => $collection])]
|
||||||
: ['label' => __('general.nav.products'), 'href' => '/products'],
|
: ['label' => __('general.nav.products'), 'href' => '/products'],
|
||||||
['label' => $product->translateAttribute('name')],
|
['label' => $product['name']],
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -26,7 +24,7 @@ class="grid grid-cols-1 md:grid-cols-2 gap-12"
|
|||||||
data-controller="product-gallery"
|
data-controller="product-gallery"
|
||||||
class="flex gap-4 items-start"
|
class="flex gap-4 items-start"
|
||||||
>
|
>
|
||||||
@if($product->media->isNotEmpty())
|
@if(!empty($product['media']))
|
||||||
{{-- Thumbnails --}}
|
{{-- Thumbnails --}}
|
||||||
<div class="flex flex-col items-center gap-1 w-[116px] shrink-0 -mt-12.5">
|
<div class="flex flex-col items-center gap-1 w-[116px] shrink-0 -mt-12.5">
|
||||||
<button
|
<button
|
||||||
@@ -41,20 +39,20 @@ class="gallery-arrow w-full flex items-center justify-center py-2"
|
|||||||
class="flex flex-col gap-4 overflow-hidden"
|
class="flex flex-col gap-4 overflow-hidden"
|
||||||
style="max-height: var(--gallery-height, 600px)"
|
style="max-height: var(--gallery-height, 600px)"
|
||||||
>
|
>
|
||||||
@foreach($product->media as $i => $media)
|
@foreach($product['media'] as $i => $media)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-action="click->product-gallery#select"
|
data-action="click->product-gallery#select"
|
||||||
data-product-gallery-target="thumb"
|
data-product-gallery-target="thumb"
|
||||||
data-src="{{ $media->getUrl() }}"
|
data-src="{{ $media['url'] }}"
|
||||||
data-alt="{{ $product->translateAttribute('name') }}"
|
data-alt="{{ $product['name'] }}"
|
||||||
class="block w-full shrink-0 border border-black overflow-hidden "
|
class="block w-full shrink-0 border border-black overflow-hidden "
|
||||||
aria-label="View image {{ $i + 1 }}"
|
aria-label="View image {{ $i + 1 }}"
|
||||||
aria-pressed="{{ $i === 0 ? 'true' : 'false' }}"
|
aria-pressed="{{ $i === 0 ? 'true' : 'false' }}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- opacity-50 transition-opacity {{ $i === 0 ? 'opacity-100' : '' }}" -->
|
<!-- opacity-50 transition-opacity {{ $i === 0 ? 'opacity-100' : '' }}" -->
|
||||||
<img src="{{ $media->getUrl() }}" alt="" class="w-full h-auto object-cover" aria-hidden="true">
|
<img src="{{ $media['url'] }}" alt="" class="w-full h-auto object-cover" aria-hidden="true">
|
||||||
</button>
|
</button>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@@ -77,8 +75,8 @@ class="flex-1 border border-black bg-white cursor-zoom-in block p-0"
|
|||||||
<img
|
<img
|
||||||
data-product-form-target="image"
|
data-product-form-target="image"
|
||||||
data-product-gallery-target="main"
|
data-product-gallery-target="main"
|
||||||
src="{{ $product->media->first()->getUrl() }}"
|
src="{{ $product['media'][0]['url'] }}"
|
||||||
alt="{{ $product->translateAttribute('name') }}"
|
alt="{{ $product['name'] }}"
|
||||||
class="w-full h-auto block"
|
class="w-full h-auto block"
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -145,19 +143,19 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
|||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
<h1 class="font-display font-medium text-4xl lg:text-[54px]">
|
<h1 class="font-display font-medium text-4xl lg:text-[54px]">
|
||||||
{{ $product->translateAttribute('name') }}
|
{{ $product['name'] }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<x-reviews-stars :rating="3" :count="24" :showCount="true" />
|
<x-reviews-stars :rating="$product['average_rating'] ?? 0" :count="$product['review_count']" :showCount="true" />
|
||||||
|
|
||||||
@if($product->variants->first()?->prices->isNotEmpty())
|
@if($product['price'] !== null)
|
||||||
<p class="text-2xl font-bold" data-product-form-target="price">
|
<p class="text-2xl font-bold" data-product-form-target="price">
|
||||||
<x-ui.price :amount="$product->variants->first()->prices->first()->price->decimal" />
|
<x-ui.price :amount="$product['price']" />
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$desc = strip_tags($product->translateAttribute('description') ?? '');
|
$desc = strip_tags($product['description'] ?? '');
|
||||||
$descTruncated = Str::limit($desc, 137);
|
$descTruncated = Str::limit($desc, 137);
|
||||||
$descNeedsMore = mb_strlen($desc) > mb_strlen(rtrim($descTruncated, '.'));
|
$descNeedsMore = mb_strlen($desc) > mb_strlen(rtrim($descTruncated, '.'));
|
||||||
@endphp
|
@endphp
|
||||||
@@ -168,8 +166,8 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($option && $product->variants->count() >= 1)
|
@if($option && !empty($product['variants']))
|
||||||
<x-ui.color-swatch :variants="$product->variants" :option="$option" />
|
<x-ui.color-swatch :variants="$product['variants']" :option="$option" />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="flex items-stretch gap-10">
|
<div class="flex items-stretch gap-10">
|
||||||
@@ -183,16 +181,12 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
|||||||
|
|
||||||
<x-ui.tabs class="mt-16" size="lg" :tabs="[
|
<x-ui.tabs class="mt-16" size="lg" :tabs="[
|
||||||
['id' => 'description', 'label' => __('general.product.description')],
|
['id' => 'description', 'label' => __('general.product.description')],
|
||||||
// ['id' => 'reviews', 'label' => __('general.product.reviews') . ' (' . count($reviews) . ')'],
|
['id' => 'reviews', 'label' => __('general.product.reviews') . ' (' . $product['review_count'] . ')'],
|
||||||
['id' => 'reviews', 'label' => __('general.product.reviews') . ' (3)'],
|
|
||||||
]">
|
]">
|
||||||
<x-slot name="description">
|
<x-slot name="description">
|
||||||
<div class="leading-7 [&_p]:mt-4">
|
<div class="leading-7 [&_p]:mt-4">
|
||||||
{!! $product->translateAttribute('description') !!}
|
{!! $product['description'] !!}
|
||||||
</div>
|
</div>
|
||||||
@if($product->translateAttribute('details'))
|
|
||||||
<div class="mt-6">{!! $product->translateAttribute('details') !!}</div>
|
|
||||||
@endif
|
|
||||||
<ul class="mt-8 flex flex-col gap-3 text-neutral-600 list-disc list-outside pl-5">
|
<ul class="mt-8 flex flex-col gap-3 text-neutral-600 list-disc list-outside pl-5">
|
||||||
<li>Όλα τα προϊόντα εκτυπώνονται και προετοιμάζονται κατά παραγγελία. Ο χρόνος προετοιμασίας κυμαίνεται μεταξύ 2 και 7 εργάσιμων ημερών.</li>
|
<li>Όλα τα προϊόντα εκτυπώνονται και προετοιμάζονται κατά παραγγελία. Ο χρόνος προετοιμασίας κυμαίνεται μεταξύ 2 και 7 εργάσιμων ημερών.</li>
|
||||||
<li>Όλα τα προϊόντα κατασκευάζονται με τρισδιάστατη εκτύπωση σε ειδικούς εκτυπωτές πλαστικού υλικού. Πιθανώς να έχουν εμφανείς γραμμές ένωσης, στρώσεις εκτύπωσης υλικού και μικρές ατέλειες. Είναι φυσιολογικό για το αποτέλεσμα αυτής της δημιουργικής διαδικασίας.</li>
|
<li>Όλα τα προϊόντα κατασκευάζονται με τρισδιάστατη εκτύπωση σε ειδικούς εκτυπωτές πλαστικού υλικού. Πιθανώς να έχουν εμφανείς γραμμές ένωσης, στρώσεις εκτύπωσης υλικού και μικρές ατέλειες. Είναι φυσιολογικό για το αποτέλεσμα αυτής της δημιουργικής διαδικασίας.</li>
|
||||||
@@ -200,10 +194,16 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
|||||||
</ul>
|
</ul>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
<x-slot name="reviews">
|
<x-slot name="reviews">
|
||||||
{{-- @if(count($reviews) > 0)
|
@if(!empty($product['reviews']))
|
||||||
<div class="mb-10">
|
<div class="mb-10">
|
||||||
@foreach($reviews as $review)
|
@foreach($product['reviews'] as $review)
|
||||||
<x-review-card :review="$review" />
|
<x-review-card :review="[
|
||||||
|
'rating' => $review['rating'],
|
||||||
|
'name' => $review['reviewer_name'],
|
||||||
|
'date' => $review['reviewed_at'] ? \Illuminate\Support\Carbon::createFromTimestamp($review['reviewed_at'])->translatedFormat('d M Y') : '',
|
||||||
|
'text' => $review['body'],
|
||||||
|
'image' => $review['media'][0]['url'] ?? null,
|
||||||
|
]" />
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
@@ -211,8 +211,8 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<h3 class="text-h4 font-bold">
|
<h3 class="text-h4 font-bold">
|
||||||
{{ count($reviews) > 0 ? 'Πρόσθεσε μια' : 'Γράψε την πρώτη' }} αξιολόγηση για το «{{ $product->translateAttribute('name') }}»
|
{{ $product['review_count'] > 0 ? 'Πρόσθεσε μια' : 'Γράψε την πρώτη' }} αξιολόγηση για το «{{ $product['name'] }}»
|
||||||
</h3> --}}
|
</h3>
|
||||||
|
|
||||||
<x-review-form :product="$product" />
|
<x-review-form :product="$product" />
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
->group(function () {
|
->group(function () {
|
||||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||||
|
|
||||||
Route::get('/products/{product}', [ProductController::class, 'show'])->name(
|
Route::get('/products/{id}', [ProductController::class, 'show'])->name(
|
||||||
'product.show',
|
'product.show',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user