generated from boboko/starter
Compare commits
3
Commits
65205e2760
...
86c645fb36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86c645fb36 | ||
|
|
0dcaf112be | ||
|
|
b3405c1b60 |
+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
|
||||
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install \
|
||||
--no-interaction \
|
||||
--no-scripts \
|
||||
--prefer-dist \
|
||||
--ignore-platform-reqs
|
||||
|
||||
# No build-time `composer install` here: composer.json's boboko/core path repo
|
||||
# (../boboko-core) isn't visible in the build context, only once bind-mounted at
|
||||
# container start — entrypoint.sh already runs composer install +
|
||||
# composer update boboko/* on every boot, so this would be redundant even if it
|
||||
# could work.
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY docker/entrypoint-worker.sh /entrypoint-worker.sh
|
||||
RUN chmod +x /entrypoint.sh /entrypoint-worker.sh
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Lunar\Models\Collection;
|
||||
use Modules\Core\Catalog\ProductFilters;
|
||||
use Modules\Core\Catalog\ProductService;
|
||||
use Modules\Core\Product\DTOs\ProductFilters;
|
||||
use Modules\Core\Product\Services\ProductService;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
@@ -19,29 +18,19 @@ public function show(string $locale, Collection $collection)
|
||||
$page = (int) request('page', 1);
|
||||
|
||||
// Listing/filtering reads from the Meilisearch index via ProductService,
|
||||
// not Eloquent — see Modules\Core\Catalog\ProductService. It returns plain
|
||||
// arrays (already localized/flattened), not Product models.
|
||||
$result = $this->products->list(
|
||||
// not Eloquent — see Modules\Core\Product\Services\ProductService. list() returns a
|
||||
// real LengthAwarePaginator of plain arrays (already localized/flattened),
|
||||
// not Product models.
|
||||
$products = $this->products->list(
|
||||
filters: new ProductFilters(collectionId: $collection->id),
|
||||
perPage: $perPage,
|
||||
page: $page,
|
||||
);
|
||||
|
||||
$products = new LengthAwarePaginator(
|
||||
items: collect($result['data'])->map(fn (array $product) => [
|
||||
'name' => $product['name'],
|
||||
'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(),
|
||||
],
|
||||
);
|
||||
)->through(fn (array $product) => [
|
||||
'name' => $product['name'],
|
||||
'price' => $product['price'],
|
||||
'image' => $product['media'][0]['url'] ?? null,
|
||||
'href' => route('product.show', ['id' => $product['id']]),
|
||||
]);
|
||||
|
||||
return view('category.show', [
|
||||
'collection' => $collection,
|
||||
|
||||
@@ -23,7 +23,7 @@ public function index(string $locale)
|
||||
'name' => $product->translateAttribute('name'),
|
||||
'price' => $product->variants->first()?->prices->first()?->price->decimal,
|
||||
'image' => $product->media->first()?->getUrl(),
|
||||
'href' => route('product.show', ['product' => $product]),
|
||||
'href' => route('product.show', ['id' => $product->id]),
|
||||
]);
|
||||
|
||||
return view('home', [
|
||||
|
||||
@@ -2,44 +2,44 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Lunar\Models\Product;
|
||||
use Illuminate\Http\Response;
|
||||
use Lunar\Models\Collection;
|
||||
use Modules\Core\Product\Services\ProductService;
|
||||
|
||||
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([
|
||||
"variants.prices.currency",
|
||||
"variants.values.option",
|
||||
"media",
|
||||
"collections",
|
||||
]);
|
||||
$product = $this->products->getById($id);
|
||||
|
||||
$option = $product->variants->first()?->values->first()?->option;
|
||||
abort_if($product === null, Response::HTTP_NOT_FOUND);
|
||||
|
||||
$variantsData = $product->variants
|
||||
->map(
|
||||
fn($v) => [
|
||||
"id" => $v->id,
|
||||
"price" => $v->prices->first()?->price->decimal,
|
||||
"image" => null, // variant-level media not differentiated yet
|
||||
],
|
||||
)
|
||||
$collection = $product['collections'][0] ?? null;
|
||||
$collectionModel = $collection !== null ? Collection::find($collection) : null;
|
||||
|
||||
$variantsData = collect($product['variants'])
|
||||
->map(fn (array $variant) => [
|
||||
'id' => $variant['id'],
|
||||
'price' => $variant['prices'][0]['price'] ?? null,
|
||||
'image' => $variant['media'][0]['url'] ?? null,
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
->all();
|
||||
|
||||
$firstImage = $product->media->first()?->getUrl();
|
||||
$firstVariant = $product['variants'][0] ?? null;
|
||||
$option = $firstVariant['options'][0]['option'] ?? null;
|
||||
|
||||
// temp categories here
|
||||
$categories = \Lunar\Models\Collection::orderBy("_lft")->get();
|
||||
$categories = Collection::orderBy('_lft')->get();
|
||||
|
||||
// dd($product);
|
||||
|
||||
return view("product.show", [
|
||||
"categories" => $categories,
|
||||
"product" => $product,
|
||||
"option" => $option,
|
||||
"variantsData" => $variantsData,
|
||||
return view('product.show', [
|
||||
'categories' => $categories,
|
||||
'collection' => $collectionModel,
|
||||
'product' => $product,
|
||||
'option' => $option,
|
||||
'variantsData' => $variantsData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,20 @@
|
||||
|
||||
'auto_create_customer_for_user' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Product Option Types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enabled `Modules\Core\Product\Contracts\ProductOptionTypeInterface`
|
||||
| implementations. An admin picks one per ProductOption from a dropdown
|
||||
| on the option's own edit form — the selection is stored in
|
||||
| ProductOption::meta, not tied to the option's handle.
|
||||
|
|
||||
*/
|
||||
|
||||
'product_option_types' => [
|
||||
\Modules\Core\Product\OptionTypes\ColorOptionType::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
Lunar\Models\Collection::class => Lunar\Search\CollectionIndexer::class,
|
||||
Lunar\Models\Customer::class => Lunar\Search\CustomerIndexer::class,
|
||||
Lunar\Models\Order::class => Lunar\Search\OrderIndexer::class,
|
||||
Lunar\Models\Product::class => Modules\Core\Search\ProductIndexer::class,
|
||||
Lunar\Models\Product::class => Modules\Core\Product\Services\ProductIndexer::class,
|
||||
Lunar\Models\ProductOption::class => Lunar\Search\ProductOptionIndexer::class,
|
||||
],
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'nav' => [
|
||||
'home' => 'Αρχική',
|
||||
'products' => 'Προϊόντα',
|
||||
'contact' => 'Επικοινωνία',
|
||||
],
|
||||
|
||||
'product' => [
|
||||
'description' => 'Περιγραφή',
|
||||
'reviews' => 'Αξιολογήσεις',
|
||||
],
|
||||
|
||||
// Laravel pluralization: {0} zero|{1} one|[2,*] many. :count is replaced automatically.
|
||||
'customer_reviews' => '{0} Καμία αξιολόγηση πελάτη|{1} :count αξιολόγηση πελάτη|[2,*] :count αξιολογήσεις πελατών',
|
||||
|
||||
'shop' => [
|
||||
// Laravel pluralization keyed on the total result count.
|
||||
'showing_results' => '{0} Δεν βρέθηκαν προϊόντα|{1} Εμφάνιση :first–:last από :total αποτέλεσμα|[2,*] Εμφάνιση :first–:last από :total αποτελέσματα',
|
||||
'no_products' => 'Δεν βρέθηκαν προϊόντα σε αυτή την κατηγορία.',
|
||||
'search_label' => 'Αναζήτηση προϊόντων',
|
||||
'search_placeholder' => 'Αναζήτησε προϊόντα…',
|
||||
'filter_price' => 'Φίλτρο τιμής',
|
||||
'apply' => 'Εφαρμογή',
|
||||
'availability' => 'Διαθεσιμότητα',
|
||||
'in_stock_only' => 'Μόνο διαθέσιμα προϊόντα',
|
||||
'sort_label' => 'Ταξινόμηση προϊόντων',
|
||||
'sort_default' => 'Προεπιλεγμένη ταξινόμηση',
|
||||
'sort_popularity' => 'Δημοφιλή',
|
||||
'sort_price_asc' => 'Τιμή: Αύξουσα',
|
||||
'sort_price_desc' => 'Τιμή: Φθίνουσα',
|
||||
'sort_newest' => 'Νεότερα',
|
||||
],
|
||||
|
||||
'pagination' => [
|
||||
'nav_label' => 'Σελιδοποίηση',
|
||||
'page' => 'Σελίδα :page',
|
||||
'next' => 'Επόμενη σελίδα',
|
||||
'previous' => 'Προηγούμενη σελίδα',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'nav' => [
|
||||
'home' => 'Home',
|
||||
'products' => 'Products',
|
||||
'contact' => 'Contact',
|
||||
],
|
||||
|
||||
'product' => [
|
||||
'description' => 'Description',
|
||||
'reviews' => 'Reviews',
|
||||
],
|
||||
|
||||
// Laravel pluralization: {0} zero|{1} one|[2,*] many. :count is replaced automatically.
|
||||
'customer_reviews' => '{0} No customer reviews|{1} :count customer review|[2,*] :count customer reviews',
|
||||
|
||||
'shop' => [
|
||||
// Laravel pluralization keyed on the total result count.
|
||||
'showing_results' => '{0} No products found|{1} Showing :first–:last of :total result|[2,*] Showing :first–:last of :total results',
|
||||
'no_products' => 'No products found in this category.',
|
||||
'search_label' => 'Search products',
|
||||
'search_placeholder' => 'Search products…',
|
||||
'filter_price' => 'Filter by price',
|
||||
'apply' => 'Apply',
|
||||
'availability' => 'Availability',
|
||||
'in_stock_only' => 'In-stock products only',
|
||||
'sort_label' => 'Sort products',
|
||||
'sort_default' => 'Default sorting',
|
||||
'sort_popularity' => 'Popularity',
|
||||
'sort_price_asc' => 'Price: Low to High',
|
||||
'sort_price_desc' => 'Price: High to Low',
|
||||
'sort_newest' => 'Newest',
|
||||
],
|
||||
|
||||
'pagination' => [
|
||||
'nav_label' => 'Pagination',
|
||||
'page' => 'Page :page',
|
||||
'next' => 'Next page',
|
||||
'previous' => 'Previous page',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -10,14 +10,14 @@
|
||||
<h1 class="font-display font-extrabold text-h1">{{ $collection->translateAttribute('name') }}</h1>
|
||||
|
||||
<x-breadcrumb :items="[
|
||||
['label' => __('general.nav.home'), 'href' => route('home')],
|
||||
['label' => __('storefront.nav.home'), 'href' => route('home')],
|
||||
['label' => $collection->translateAttribute('name')],
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-6 flex-wrap border-b border-black pb-6 mb-10">
|
||||
<p class="text-neutral-600">
|
||||
{{ trans_choice('general.shop.showing_results', $products->total(), [
|
||||
{{ trans_choice('storefront.shop.showing_results', $products->total(), [
|
||||
'first' => $products->firstItem() ?? 0,
|
||||
'last' => $products->lastItem() ?? 0,
|
||||
'total' => $products->total(),
|
||||
@@ -26,13 +26,13 @@
|
||||
|
||||
{{-- Dummy — not wired to real sorting yet --}}
|
||||
<x-ui.select
|
||||
:ariaLabel="__('general.shop.sort_label')"
|
||||
:ariaLabel="__('storefront.shop.sort_label')"
|
||||
:options="[
|
||||
['value' => 'default', 'label' => __('general.shop.sort_default')],
|
||||
['value' => 'popularity', 'label' => __('general.shop.sort_popularity')],
|
||||
['value' => 'price-asc', 'label' => __('general.shop.sort_price_asc')],
|
||||
['value' => 'price-desc', 'label' => __('general.shop.sort_price_desc')],
|
||||
['value' => 'newest', 'label' => __('general.shop.sort_newest')],
|
||||
['value' => 'default', 'label' => __('storefront.shop.sort_default')],
|
||||
['value' => 'popularity', 'label' => __('storefront.shop.sort_popularity')],
|
||||
['value' => 'price-asc', 'label' => __('storefront.shop.sort_price_asc')],
|
||||
['value' => 'price-desc', 'label' => __('storefront.shop.sort_price_desc')],
|
||||
['value' => 'newest', 'label' => __('storefront.shop.sort_newest')],
|
||||
]"
|
||||
value="default"
|
||||
/>
|
||||
@@ -43,7 +43,7 @@
|
||||
{{-- Products --}}
|
||||
<div>
|
||||
@if($products->isEmpty())
|
||||
<p class="text-neutral-500">{{ __('general.shop.no_products') }}</p>
|
||||
<p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p>
|
||||
@else
|
||||
<x-product-grid :products="$products->items()" cols="3" />
|
||||
|
||||
@@ -57,18 +57,18 @@
|
||||
<aside class="flex flex-col gap-10">
|
||||
|
||||
<div>
|
||||
<label for="shop-search" class="sr-only">{{ __('general.shop.search_label') }}</label>
|
||||
<label for="shop-search" class="sr-only">{{ __('storefront.shop.search_label') }}</label>
|
||||
<div class="relative">
|
||||
<x-ui.input
|
||||
type="search"
|
||||
id="shop-search"
|
||||
:placeholder="__('general.shop.search_placeholder')"
|
||||
:placeholder="__('storefront.shop.search_placeholder')"
|
||||
class="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
aria-label="{{ __('general.shop.search_label') }}"
|
||||
aria-label="{{ __('storefront.shop.search_label') }}"
|
||||
>
|
||||
<x-ui.icon name="search" :size="20" />
|
||||
</button>
|
||||
@@ -76,7 +76,7 @@ class="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="font-display font-extrabold uppercase">{{ __('general.shop.filter_price') }}</h2>
|
||||
<h2 class="font-display font-extrabold uppercase">{{ __('storefront.shop.filter_price') }}</h2>
|
||||
|
||||
<div class="flex items-center gap-2" aria-hidden="true">
|
||||
<x-ui.icon name="arrow-left" :size="20" />
|
||||
@@ -87,15 +87,15 @@ class="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<span class="text-sm">€10 - €50</span>
|
||||
<button type="button" class="underline-slide [--slide-h:1px] font-display font-bold italic uppercase text-sm">
|
||||
{{ __('general.shop.apply') }}
|
||||
{{ __('storefront.shop.apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="font-display font-extrabold uppercase">{{ __('general.shop.availability') }}</h2>
|
||||
<h2 class="font-display font-extrabold uppercase">{{ __('storefront.shop.availability') }}</h2>
|
||||
<x-ui.checkbox id="shop-in-stock" name="in_stock">
|
||||
{{ __('general.shop.in_stock_only') }}
|
||||
{{ __('storefront.shop.in_stock_only') }}
|
||||
</x-ui.checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{{-- Products (CSS-only hover dropdown) --}}
|
||||
<div class="group relative h-full flex items-center">
|
||||
<a href="{{ url('/'.app()->getLocale().'/products') }}" class="nav-link uppercase inline-flex items-center gap-1 relative font-display font-extrabold italic text-black no-underline py-8">
|
||||
<span>{{ __('general.nav.products') }}</span>
|
||||
<span>{{ __('storefront.nav.products') }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4 not-italic transition-transform group-hover:rotate-180">
|
||||
<path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Contact --}}
|
||||
<a href="{{ route('contact') }}" class="nav-link uppercase inline-flex items-center gap-1 relative font-display font-extrabold italic text-black no-underline"><span>{{ __('general.nav.contact') }}</span></a>
|
||||
<a href="{{ route('contact') }}" class="nav-link uppercase inline-flex items-center gap-1 relative font-display font-extrabold italic text-black no-underline"><span>{{ __('storefront.nav.contact') }}</span></a>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ class="flex flex-col gap-8 mt-10"
|
||||
@csrf
|
||||
|
||||
{{-- Rating --}}
|
||||
<x-ui.field label="Βαθμολογία" for="rating" :required="true">
|
||||
<x-ui.field :label="__('storefront.review.rating')" for="rating" :required="true">
|
||||
<div
|
||||
id="rating"
|
||||
data-controller="star-rating"
|
||||
class="flex items-center gap-1.5 text-brand"
|
||||
role="radiogroup"
|
||||
aria-label="Βαθμολογία"
|
||||
aria-label="{{ __('storefront.review.rating') }}"
|
||||
aria-required="true"
|
||||
>
|
||||
<input type="hidden" name="rating" value="0" data-star-rating-target="input">
|
||||
@@ -24,7 +24,7 @@ class="flex items-center gap-1.5 text-brand"
|
||||
data-star-rating-target="star"
|
||||
data-value="{{ $i }}"
|
||||
data-action="click->star-rating#select mouseenter->star-rating#hover mouseleave->star-rating#leave"
|
||||
aria-label="{{ $i }} {{ $i === 1 ? 'αστέρι' : 'αστέρια' }}"
|
||||
aria-label="{{ trans_choice('storefront.review.stars_count', $i, ['count' => $i]) }}"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<svg
|
||||
@@ -43,24 +43,24 @@ class="w-6 h-6"
|
||||
</div>
|
||||
</x-ui.field>
|
||||
|
||||
<x-ui.field label="Γράψε μια αξιολόγηση" for="review-content" :required="true">
|
||||
<x-ui.field :label="__('storefront.review.write_label')" for="review-content" :required="true">
|
||||
<x-ui.textarea id="review-content" name="content" :required="true" />
|
||||
</x-ui.field>
|
||||
|
||||
<x-ui.field label="Όνομα" for="review-name" labelDescription="Προαιρετικό">
|
||||
<x-ui.field :label="__('storefront.review.name')" for="review-name" :labelDescription="__('storefront.review.name_optional')">
|
||||
<x-ui.input id="review-name" name="name" autocomplete="name" />
|
||||
</x-ui.field>
|
||||
|
||||
<x-ui.field label="Email" for="review-email" :required="true" labelDescription="Δεν θα δημοσιευτεί">
|
||||
<x-ui.field :label="__('storefront.review.email')" for="review-email" :required="true" :labelDescription="__('storefront.review.email_not_published')">
|
||||
<x-ui.input id="review-email" name="email" type="email" :required="true" autocomplete="email" />
|
||||
</x-ui.field>
|
||||
|
||||
<x-ui.checkbox id="review-save-info" name="save_info" >
|
||||
<span class="text-sm">Αποθήκευσε το όνομα και το email μου για την επόμενη φορά που θα σχολιάσω.</span>
|
||||
<span class="text-sm">{{ __('storefront.review.save_info') }}</span>
|
||||
</x-ui.checkbox>
|
||||
|
||||
<div>
|
||||
<x-ui.button type="submit">Υποβολή</x-ui.button>
|
||||
<x-ui.button type="submit">{{ __('storefront.review.submit') }}</x-ui.button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -29,7 +29,7 @@ class="flex items-center gap-1.5 text-brand"
|
||||
|
||||
@if ($showCount && $count > 0)
|
||||
<span class="text-sm text-neutral-500">
|
||||
({{ trans_choice('general.customer_reviews', $count, ['count' => $count]) }})
|
||||
({{ trans_choice('storefront.customer_reviews', $count, ['count' => $count]) }})
|
||||
</span>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
{{-- $variants: array of Modules\Core\Product\Services\ProductIndexer's mapVariant()
|
||||
shape (id, options: [{option, value, meta}], ...) — plain arrays, not Eloquent
|
||||
models, since this is fed from Modules\Core\Product\Services\ProductService. --}}
|
||||
@props(['variants', 'option' => null])
|
||||
|
||||
<div {{ $attributes }}>
|
||||
@if($option)
|
||||
<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>
|
||||
@endif
|
||||
|
||||
<div
|
||||
class="flex flex-wrap gap-2"
|
||||
role="group"
|
||||
aria-label="{{ $option?->translate('name') ?? 'Color' }}"
|
||||
aria-label="{{ $option ?? 'Color' }}"
|
||||
>
|
||||
@foreach($variants as $variant)
|
||||
@php
|
||||
$value = $variant->values->first();
|
||||
$label = $value?->translate('name') ?? '';
|
||||
$bg = $value?->meta['hex'] ?? '#cccccc';
|
||||
$value = $variant['options'][0] ?? null;
|
||||
$label = $value['value'] ?? '';
|
||||
$bg = $value['meta']['hex'] ?? '#cccccc';
|
||||
@endphp
|
||||
<button
|
||||
type="button"
|
||||
class="color-swatch"
|
||||
data-product-form-target="swatch"
|
||||
data-action="click->product-form#selectVariant"
|
||||
data-variant-id="{{ $variant->id }}"
|
||||
data-variant-id="{{ $variant['id'] }}"
|
||||
style="background-color: {{ $bg }};"
|
||||
aria-label="{{ $label }}"
|
||||
aria-pressed="false"
|
||||
|
||||
@@ -9,21 +9,21 @@
|
||||
--}}
|
||||
|
||||
@if($paginator->hasPages())
|
||||
<nav aria-label="{{ __('general.pagination.nav_label') }}" {{ $attributes->merge(['class' => 'flex items-center gap-6 font-display font-bold']) }}>
|
||||
<nav aria-label="{{ __('storefront.pagination.nav_label') }}" {{ $attributes->merge(['class' => 'flex items-center gap-6 font-display font-bold']) }}>
|
||||
<ol class="flex items-center gap-6">
|
||||
@foreach($paginator->getUrlRange(1, $paginator->lastPage()) as $page => $url)
|
||||
<li>
|
||||
@if($page === $paginator->currentPage())
|
||||
<span class="underline-slide is-active" aria-current="page">{{ str_pad((string) $page, 2, '0', STR_PAD_LEFT) }}</span>
|
||||
@else
|
||||
<a href="{{ $url }}" class="underline-slide" aria-label="{{ __('general.pagination.page', ['page' => $page]) }}">{{ str_pad((string) $page, 2, '0', STR_PAD_LEFT) }}</a>
|
||||
<a href="{{ $url }}" class="underline-slide" aria-label="{{ __('storefront.pagination.page', ['page' => $page]) }}">{{ str_pad((string) $page, 2, '0', STR_PAD_LEFT) }}</a>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
|
||||
@if($paginator->hasMorePages())
|
||||
<a href="{{ $paginator->nextPageUrl() }}" aria-label="{{ __('general.pagination.next') }}">
|
||||
<a href="{{ $paginator->nextPageUrl() }}" aria-label="{{ __('storefront.pagination.next') }}">
|
||||
<x-ui.icon name="arrow-right" :size="24" />
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@@ -17,13 +17,13 @@ class="w-full h-auto block"
|
||||
>
|
||||
@else
|
||||
<div class="w-full aspect-square bg-neutral-300 flex items-center justify-center text-neutral-500 text-sm">
|
||||
Χωρίς εικόνα
|
||||
{{ __('storefront.product.no_image') }}
|
||||
</div>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<x-ui.button size="md" position="absolute" class="opacity-0 group-hover:opacity-100 transition-opacity duration-100">
|
||||
Προσθήκη στο καλάθι
|
||||
{{ __('storefront.product.add_to_cart') }}
|
||||
</x-ui.button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', __('general.nav.contact'))
|
||||
@section('title', __('storefront.nav.contact'))
|
||||
@section('description', 'Επικοινώνησε μαζί μας για οποιαδήποτε απορία ή ιδέα έχεις και θα σου απαντήσουμε το συντομότερο δυνατό.')
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -56,12 +56,12 @@ class="{{ $loop->first ? '' : 'hidden' }} group h-full flex flex-col justify-cen
|
||||
class="w-full h-full object-cover"
|
||||
>
|
||||
@else
|
||||
<span class="text-neutral-500 text-sm">Χωρίς εικόνα</span>
|
||||
<span class="text-neutral-500 text-sm">{{ __('storefront.product.no_image') }}</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<x-ui.button size="md" position="absolute" class="opacity-0 group-hover:opacity-100 transition-opacity duration-100">
|
||||
Προσθήκη στο καλάθι
|
||||
{{ __('storefront.product.add_to_cart') }}
|
||||
</x-ui.button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $product->translateAttribute('name') . ' — ' . config('app.name'))
|
||||
@section('description', $product->translateAttribute('description'))
|
||||
@section('title', $product['name'] . ' — ' . config('app.name'))
|
||||
@section('description', $product['description'])
|
||||
|
||||
@section('content')
|
||||
<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="[
|
||||
$collection
|
||||
? ['label' => $collection->translateAttribute('name'), 'href' => route('category.show', ['collection' => $collection])]
|
||||
: ['label' => __('general.nav.products'), 'href' => '/products'],
|
||||
['label' => $product->translateAttribute('name')],
|
||||
: ['label' => __('storefront.nav.products'), 'href' => '/products'],
|
||||
['label' => $product['name']],
|
||||
]" />
|
||||
|
||||
<div
|
||||
@@ -26,7 +24,7 @@ class="grid grid-cols-1 md:grid-cols-2 gap-12"
|
||||
data-controller="product-gallery"
|
||||
class="flex gap-4 items-start"
|
||||
>
|
||||
@if($product->media->isNotEmpty())
|
||||
@if(!empty($product['media']))
|
||||
{{-- Thumbnails --}}
|
||||
<div class="flex flex-col items-center gap-1 w-[116px] shrink-0 -mt-12.5">
|
||||
<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"
|
||||
style="max-height: var(--gallery-height, 600px)"
|
||||
>
|
||||
@foreach($product->media as $i => $media)
|
||||
@foreach($product['media'] as $i => $media)
|
||||
<button
|
||||
type="button"
|
||||
data-action="click->product-gallery#select"
|
||||
data-product-gallery-target="thumb"
|
||||
data-src="{{ $media->getUrl() }}"
|
||||
data-alt="{{ $product->translateAttribute('name') }}"
|
||||
data-src="{{ $media['url'] }}"
|
||||
data-alt="{{ $product['name'] }}"
|
||||
class="block w-full shrink-0 border border-black overflow-hidden "
|
||||
aria-label="View image {{ $i + 1 }}"
|
||||
aria-pressed="{{ $i === 0 ? 'true' : 'false' }}"
|
||||
>
|
||||
|
||||
<!-- 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>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -77,8 +75,8 @@ class="flex-1 border border-black bg-white cursor-zoom-in block p-0"
|
||||
<img
|
||||
data-product-form-target="image"
|
||||
data-product-gallery-target="main"
|
||||
src="{{ $product->media->first()->getUrl() }}"
|
||||
alt="{{ $product->translateAttribute('name') }}"
|
||||
src="{{ $product['media'][0]['url'] }}"
|
||||
alt="{{ $product['name'] }}"
|
||||
class="w-full h-auto block"
|
||||
>
|
||||
</button>
|
||||
@@ -136,7 +134,7 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
||||
</div>
|
||||
@else
|
||||
<div class="w-full bg-neutral-300 flex items-center justify-center text-neutral-500 min-h-64">
|
||||
No image
|
||||
{{ __('storefront.product.no_image') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -145,36 +143,36 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<h1 class="font-display font-medium text-4xl lg:text-[54px]">
|
||||
{{ $product->translateAttribute('name') }}
|
||||
{{ $product['name'] }}
|
||||
</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">
|
||||
<x-ui.price :amount="$product->variants->first()->prices->first()->price->decimal" />
|
||||
<x-ui.price :amount="$product['price']" />
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$desc = strip_tags($product->translateAttribute('description') ?? '');
|
||||
$desc = strip_tags($product['description'] ?? '');
|
||||
$descTruncated = Str::limit($desc, 137);
|
||||
$descNeedsMore = mb_strlen($desc) > mb_strlen(rtrim($descTruncated, '.'));
|
||||
@endphp
|
||||
<div class="text-base leading-relaxed">
|
||||
{{ $descTruncated }}
|
||||
@if($descNeedsMore)
|
||||
<a href="#tab-panel-description" class="underline-slide font-semibold whitespace-nowrap">Περισσότερα</a>
|
||||
<a href="#tab-panel-description" class="underline-slide font-semibold whitespace-nowrap">{{ __('storefront.product.read_more') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($option && $product->variants->count() >= 1)
|
||||
<x-ui.color-swatch :variants="$product->variants" :option="$option" />
|
||||
@if($option && !empty($product['variants']))
|
||||
<x-ui.color-swatch :variants="$product['variants']" :option="$option" />
|
||||
@endif
|
||||
|
||||
<div class="flex items-stretch gap-10">
|
||||
<x-ui.quantity name="quantity" />
|
||||
<x-ui.button class="flex-1">Προσθήκη στο καλάθι</x-ui.button>
|
||||
<x-ui.button class="flex-1">{{ __('storefront.product.add_to_cart') }}</x-ui.button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -182,17 +180,13 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
||||
</div>
|
||||
|
||||
<x-ui.tabs class="mt-16" size="lg" :tabs="[
|
||||
['id' => 'description', 'label' => __('general.product.description')],
|
||||
// ['id' => 'reviews', 'label' => __('general.product.reviews') . ' (' . count($reviews) . ')'],
|
||||
['id' => 'reviews', 'label' => __('general.product.reviews') . ' (3)'],
|
||||
['id' => 'description', 'label' => __('storefront.product.description')],
|
||||
['id' => 'reviews', 'label' => __('storefront.product.reviews') . ' (' . $product['review_count'] . ')'],
|
||||
]">
|
||||
<x-slot name="description">
|
||||
<div class="leading-7 [&_p]:mt-4">
|
||||
{!! $product->translateAttribute('description') !!}
|
||||
{!! $product['description'] !!}
|
||||
</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">
|
||||
<li>Όλα τα προϊόντα εκτυπώνονται και προετοιμάζονται κατά παραγγελία. Ο χρόνος προετοιμασίας κυμαίνεται μεταξύ 2 και 7 εργάσιμων ημερών.</li>
|
||||
<li>Όλα τα προϊόντα κατασκευάζονται με τρισδιάστατη εκτύπωση σε ειδικούς εκτυπωτές πλαστικού υλικού. Πιθανώς να έχουν εμφανείς γραμμές ένωσης, στρώσεις εκτύπωσης υλικού και μικρές ατέλειες. Είναι φυσιολογικό για το αποτέλεσμα αυτής της δημιουργικής διαδικασίας.</li>
|
||||
@@ -200,19 +194,26 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
||||
</ul>
|
||||
</x-slot>
|
||||
<x-slot name="reviews">
|
||||
{{-- @if(count($reviews) > 0)
|
||||
@if(!empty($product['reviews']))
|
||||
<div class="mb-10">
|
||||
@foreach($reviews as $review)
|
||||
<x-review-card :review="$review" />
|
||||
@foreach($product['reviews'] as $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
|
||||
</div>
|
||||
@else
|
||||
<p class="text-neutral-500 mb-10">Δεν υπάρχουν αξιολογήσεις ακόμα.</p>
|
||||
<p class="text-neutral-500 mb-10">{{ __('storefront.review.no_reviews_yet') }}</p>
|
||||
@endif
|
||||
|
||||
<h3 class="text-h4 font-bold">
|
||||
{{ count($reviews) > 0 ? 'Πρόσθεσε μια' : 'Γράψε την πρώτη' }} αξιολόγηση για το «{{ $product->translateAttribute('name') }}»
|
||||
</h3> --}}
|
||||
{{ $product['review_count'] > 0 ? __('storefront.review.write_new') : __('storefront.review.write_first') }}
|
||||
{{ __('storefront.review.for_product', ['name' => $product['name']]) }}
|
||||
</h3>
|
||||
|
||||
<x-review-form :product="$product" />
|
||||
</x-slot>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
->group(function () {
|
||||
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',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user