filter with ajax
This commit is contained in:
parent
80f6dc8612
commit
c71b7ff5c2
|
|
@ -66,6 +66,7 @@ class ProductController extends Controller
|
|||
$page = $request->page ?? 1;
|
||||
|
||||
$search = $request->search;
|
||||
|
||||
$filter = $request->filter ?? [];
|
||||
$sortBy = $request->sort_by ?? 'relevance';
|
||||
|
||||
|
|
@ -102,15 +103,23 @@ class ProductController extends Controller
|
|||
'price_range_end' => $price_range_end,
|
||||
]);
|
||||
|
||||
// Check if there are more products
|
||||
$hasMore = count($products) >= $limit;
|
||||
|
||||
// Render product cards HTML
|
||||
$productHtml = '';
|
||||
foreach ($products as $product) {
|
||||
$productHtml .= '<div class="col-6 col-md-4 mb-2 mb-sm-3 mb-md-0">';
|
||||
$productHtml .= view('components.home.product-card', ['product' => $product])->render();
|
||||
if (count($products) == 0) {
|
||||
$productHtml = '<div class="col-12">';
|
||||
$productHtml .= 'Pencarian tidak ditemukan';
|
||||
$productHtml .= '</div>';
|
||||
} else {
|
||||
foreach ($products as $product) {
|
||||
$productHtml .= '<div class="col-6 col-md-4 mb-2 mb-sm-3 mb-md-0">';
|
||||
$productHtml .= view('components.home.product-card', ['product' => $product])->render();
|
||||
$productHtml .= '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// filter
|
||||
$filter = $request->filter ?? [];
|
||||
if (isset($filter['category']) && $filter['category']){
|
||||
|
|
@ -123,7 +132,6 @@ class ProductController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (isset($filter['gender']) && $filter['gender']) {
|
||||
$gender = Gender::find($filter['gender']);
|
||||
|
||||
|
|
@ -141,13 +149,13 @@ class ProductController extends Controller
|
|||
'filters' => $filters,
|
||||
'products' => $productHtml,
|
||||
'count' => count($products),
|
||||
'has_more' => count($products) >= $limit
|
||||
'has_more' => $hasMore,
|
||||
'current_page' => $page
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
|
||||
|
||||
$productRepository = new ProductRepository;
|
||||
$products = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,83 +6,57 @@ use App\Models\DiscountItem;
|
|||
use App\Models\Items;
|
||||
use App\Models\StoreCategoryMap;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProductRepository
|
||||
{
|
||||
public function getList($params)
|
||||
{
|
||||
$event = @$params["event"];
|
||||
$sort = @$params["sort"];
|
||||
$search = @$params["search"];
|
||||
$category_id = @$params["category_id"];
|
||||
$brand_id = @$params["brand_id"];
|
||||
$gender_id = @$params["gender_id"];
|
||||
$limit = @$params["limit"];
|
||||
{
|
||||
$event = @$params['event'];
|
||||
$sort = @$params['sort'];
|
||||
$search = @$params['search'];
|
||||
$category_id = @$params['category_id'];
|
||||
$brand_id = @$params['brand_id'];
|
||||
$gender_id = @$params['gender_id'];
|
||||
$limit = @$params['limit'];
|
||||
|
||||
$location_id = @$params["location_id"];
|
||||
$is_consignment = @$params["is_consignment"];
|
||||
|
||||
$price_range_start = @$params["price_range_start"];
|
||||
$price_range_end = @$params["price_range_end"];
|
||||
$location_id = @$params['location_id'];
|
||||
$is_consignment = @$params['is_consignment'];
|
||||
|
||||
$price_range_start = @$params['price_range_start'];
|
||||
$price_range_end = @$params['price_range_end'];
|
||||
|
||||
$sorting_ids = [];
|
||||
if ($sort == "price_low_to_high" || $sort == "price_high_to_low"){
|
||||
$sorting_ids = DiscountItem::whereHas('discount', function($query) use ($location_id, $is_consignment) {
|
||||
$query->where('type', 'price')
|
||||
->where(function ($query) {
|
||||
$query->where('valid_at', '<=', now())
|
||||
->orWhereNull('valid_at');
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', now())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where(function ($query) use ($location_id, $is_consignment) {
|
||||
if (! $is_consignment) {
|
||||
$query->whereNull('discounts.location_id');
|
||||
}
|
||||
|
||||
if ($location_id) {
|
||||
$query->orWhere('discounts.location_id', $location_id);
|
||||
}
|
||||
});
|
||||
})
|
||||
->orderBy('discount_items.price', $sort == "price_low_to_high" ? 'asc' : 'desc')
|
||||
->pluck('item_reference_id')->toArray();
|
||||
if ($sort == 'price_low_to_high' || $sort == 'price_high_to_low') {
|
||||
$params_filter = [];
|
||||
$params_filter['location_id'] = $location_id;
|
||||
$params_filter['sort'] = $sort;
|
||||
$sorting_ids = collect($this->getProductList($params_filter))->pluck('id')->toArray();
|
||||
|
||||
// Log::info($sorting_ids);
|
||||
}
|
||||
|
||||
$where_ids = [];
|
||||
if ($price_range_start && $price_range_end) {
|
||||
$where_ids = DiscountItem::whereHas('discount', function($query) use ($location_id, $is_consignment, $price_range_start, $price_range_end) {
|
||||
$query->where('type', 'price')
|
||||
->where(function ($query) {
|
||||
$query->where('valid_at', '<=', now())
|
||||
->orWhereNull('valid_at');
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->where('expired_at', '>', now())
|
||||
->orWhereNull('expired_at');
|
||||
})
|
||||
->where(function ($query) use ($location_id, $is_consignment) {
|
||||
if (! $is_consignment) {
|
||||
$query->whereNull('discounts.location_id');
|
||||
}
|
||||
$params_filter = [];
|
||||
$params_filter['location_id'] = $location_id;
|
||||
$params_filter['price_range_start'] = $price_range_start;
|
||||
$params_filter['price_range_end'] = $price_range_end;
|
||||
|
||||
$where_ids = collect($this->getProductList($params_filter))->pluck('id')->toArray();
|
||||
|
||||
if (! empty($sorting_ids) && count($sorting_ids) > 0) {
|
||||
$where_ids = array_values(
|
||||
array_intersect($sorting_ids, $where_ids)
|
||||
);
|
||||
}
|
||||
|
||||
if ($location_id) {
|
||||
$query->orWhere('discounts.location_id', $location_id);
|
||||
}
|
||||
})
|
||||
->where('discount_items.price', '>=', $price_range_start)
|
||||
->where('discount_items.price', '<=', $price_range_end);
|
||||
})
|
||||
->pluck('item_reference_id')->toArray();
|
||||
}
|
||||
|
||||
$builder = Items::select('items.*','percent')
|
||||
$builder = Items::select('items.*', 'percent')
|
||||
->leftJoin('item_dimension', 'item_dimension.no', 'items.number')
|
||||
->leftJoin(DB::raw("(select distinct on (item_id) item_id, percent, discounts.created_at,
|
||||
->leftJoin(DB::raw("(select distinct on (item_id) item_id, percent, discounts.created_at,
|
||||
case when discounts.valid_at is null then 'special-offer' else 'limited-sale' end as event
|
||||
from discount_items
|
||||
left join item_reference on item_reference_id = item_reference.id
|
||||
|
|
@ -91,32 +65,38 @@ class ProductRepository
|
|||
( discounts.valid_at is null or discounts.valid_at <= now()) and
|
||||
( discounts.valid_at is null or discounts.expired_at >= now())
|
||||
order by item_id, discounts.created_at desc
|
||||
) as d"),"d.item_id","=","items.id")
|
||||
->when(true, function($query) use ($event, $sort,$sorting_ids){
|
||||
if ($event){
|
||||
$query->orderByRaw("case when brand = 'CHAMELO' then 1 else 2 end ASC ");
|
||||
) as d"), 'd.item_id', '=', 'items.id')
|
||||
->when(true, function ($query) use ($event, $sort, $sorting_ids) {
|
||||
if ($event) {
|
||||
$query->orderByRaw("case when brand = 'CHAMELO' then 1 else 2 end ASC ");
|
||||
|
||||
$query->orderBy("percent", "desc");
|
||||
}else{
|
||||
$query->orderBy('percent', 'desc');
|
||||
} else {
|
||||
|
||||
/* if ($event == "special-offer"){
|
||||
$query->orderByRaw("case when brand = 'CHAMELO' then 1 else 2 end ASC ");
|
||||
} */
|
||||
if (! empty($sorting_ids) && count($sorting_ids) > 0) {
|
||||
|
||||
$caseStatements = [];
|
||||
foreach ($sorting_ids as $index => $id) {
|
||||
$caseStatements[] = "WHEN items.id = {$id} THEN " . ($index + 1);
|
||||
}
|
||||
$caseStatements[] = "ELSE 999";
|
||||
|
||||
$query->orderByRaw("
|
||||
CASE
|
||||
" . implode("\n ", $caseStatements) . "
|
||||
END ASC
|
||||
");
|
||||
|
||||
if (!empty($sorting_ids)) {
|
||||
$ids = implode(',', $sorting_ids);
|
||||
$query->orderByRaw("array_position(ARRAY[$ids], items.id)");
|
||||
}else if ($sort == "new"){
|
||||
$query->orderByRaw("case when category1 in ('CLUBS','CLUB','COMPONENT HEAD') and brand = 'PXG' then 1 else 2 end ASC");
|
||||
} else {
|
||||
$query->orderByRaw("case when d.created_at is not null then 1 else 2 end ASC, random()");
|
||||
}
|
||||
} elseif ($sort == 'new') {
|
||||
$query->orderByRaw("case when category1 in ('CLUBS','CLUB','COMPONENT HEAD') and brand = 'PXG' then 1 else 2 end ASC");
|
||||
} else {
|
||||
$query->orderByRaw('case when d.created_at is not null then 1 else 2 end ASC, random()');
|
||||
}
|
||||
|
||||
// $query->orderByRaw("items.created_at desc NULLS LAST");
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
->where("is_publish", true)
|
||||
})
|
||||
->where('is_publish', true)
|
||||
->when($search, function ($query) use ($search) {
|
||||
$query->where(function ($query) use ($search) {
|
||||
$query->where('number', 'ILIKE', "%$search%")
|
||||
|
|
@ -130,8 +110,11 @@ class ProductRepository
|
|||
->when($gender_id, function ($query) use ($gender_id) {
|
||||
$query->where('gender_id', $gender_id);
|
||||
})
|
||||
->when($event, function($query) use ($event){
|
||||
->when($event, function ($query) use ($event) {
|
||||
$query->where('d.event', $event);
|
||||
})
|
||||
->when($where_ids, function ($query) use ($where_ids) {
|
||||
$query->whereIn('items.id', $where_ids);
|
||||
});
|
||||
|
||||
if ($category_id) {
|
||||
|
|
@ -155,10 +138,128 @@ class ProductRepository
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
return $builder->paginate($limit);
|
||||
}
|
||||
|
||||
public function getProductList($params)
|
||||
{
|
||||
$bindings = [];
|
||||
|
||||
// =========================
|
||||
// ORDER BY
|
||||
// =========================
|
||||
$order_by = 'ORDER BY dpr.discount_price ASC NULLS LAST';
|
||||
|
||||
$sort = $params['sort'] ?? null;
|
||||
if ($sort === 'price_high_to_low') {
|
||||
$order_by = 'ORDER BY dpr.discount_price DESC NULLS LAST';
|
||||
} elseif ($sort === 'price_low_to_high') {
|
||||
$order_by = 'ORDER BY dpr.discount_price ASC NULLS LAST';
|
||||
} elseif ($sort === 'new') {
|
||||
$order_by = 'ORDER BY items.created_at DESC NULLS LAST';
|
||||
}
|
||||
|
||||
$location_id = $params['location_id'] ?? null;
|
||||
|
||||
// =========================
|
||||
// WHERE CONDITIONS
|
||||
// =========================
|
||||
$wheres = [];
|
||||
|
||||
$price_range_start = $params['price_range_start'] ?? null;
|
||||
$price_range_end = $params['price_range_end'] ?? null;
|
||||
|
||||
if ($price_range_start !== null && $price_range_end !== null) {
|
||||
$wheres[] = '(dpr.discount_price BETWEEN ? AND ?)';
|
||||
$bindings[] = $price_range_start;
|
||||
$bindings[] = $price_range_end;
|
||||
}
|
||||
|
||||
$whereSql = $wheres
|
||||
? 'AND '.implode(' AND ', $wheres)
|
||||
: '';
|
||||
|
||||
// =========================
|
||||
// QUERY
|
||||
// =========================
|
||||
$sql = "SELECT
|
||||
items.*,
|
||||
dp.percent,
|
||||
dp.event,
|
||||
dpr.discount_price
|
||||
FROM items
|
||||
LEFT JOIN item_dimension
|
||||
ON item_dimension.no = items.number
|
||||
|
||||
-- =========================
|
||||
-- DISCOUNT PERCENT
|
||||
-- =========================
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ON (di.item_reference_id)
|
||||
di.item_reference_id,
|
||||
di.percent,
|
||||
d.created_at,
|
||||
d.location_id,
|
||||
CASE
|
||||
WHEN d.valid_at IS NULL THEN 'special-offer'
|
||||
ELSE 'limited-sale'
|
||||
END AS event
|
||||
FROM discount_items di
|
||||
JOIN discounts d ON d.id = di.discount_id
|
||||
WHERE d.type = 'discount'
|
||||
AND (d.valid_at IS NULL OR d.valid_at <= NOW())
|
||||
AND (d.expired_at IS NULL OR d.expired_at >= NOW())
|
||||
AND (
|
||||
(22 IS NOT NULL AND (d.location_id = 22 OR d.location_id IS NULL))
|
||||
OR (22 IS NULL AND d.location_id IS NULL)
|
||||
)
|
||||
ORDER BY
|
||||
di.item_reference_id,
|
||||
CASE
|
||||
WHEN d.location_id = 22 THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
d.created_at DESC
|
||||
) dp ON dp.item_reference_id = items.id
|
||||
|
||||
-- =========================
|
||||
-- DISCOUNT PRICE
|
||||
-- =========================
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ON (di.item_reference_id)
|
||||
di.item_reference_id,
|
||||
di.price AS discount_price,
|
||||
d.created_at,
|
||||
d.location_id
|
||||
FROM discount_items di
|
||||
JOIN discounts d ON d.id = di.discount_id
|
||||
WHERE d.type = 'price'
|
||||
AND (d.valid_at IS NULL OR d.valid_at <= NOW())
|
||||
AND (d.expired_at IS NULL OR d.expired_at >= NOW())
|
||||
AND (
|
||||
(22 IS NOT NULL AND (d.location_id = 22 OR d.location_id IS NULL))
|
||||
OR (22 IS NULL AND d.location_id IS NULL)
|
||||
)
|
||||
ORDER BY
|
||||
di.item_reference_id,
|
||||
CASE
|
||||
WHEN d.location_id = 22 THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
d.created_at DESC
|
||||
) dpr ON dpr.item_reference_id = items.id
|
||||
|
||||
WHERE items.is_publish = TRUE
|
||||
AND items.deleted_at IS NULL
|
||||
$whereSql
|
||||
$order_by
|
||||
";
|
||||
|
||||
|
||||
$select= DB::select($sql, $bindings);
|
||||
|
||||
return $select;
|
||||
}
|
||||
|
||||
public function getMinMaxPrice()
|
||||
{
|
||||
|
|
@ -167,14 +268,14 @@ class ProductRepository
|
|||
'max' => DiscountItem::max('price'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function show($slug)
|
||||
{
|
||||
$product = Items::where('slug', $slug)->first();
|
||||
|
||||
|
||||
$product = Items::where('slug', $slug)
|
||||
->when(is_int($slug), function ($query) use ($slug) {
|
||||
return $query->orWhere('id', $slug);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($product == null) {
|
||||
abort(404);
|
||||
|
|
@ -193,10 +294,10 @@ class ProductRepository
|
|||
if (empty($ids)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
|
||||
// Remove except_ids from the list
|
||||
$ids = array_diff($ids, (array)$except_ids);
|
||||
|
||||
$ids = array_diff($ids, (array) $except_ids);
|
||||
|
||||
return Items::whereIn('id', $ids)->inRandomOrder()->take(10)->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
"title" => "Catalog",
|
||||
'categories' => 'Categories',
|
||||
'genders' => 'Genders',
|
||||
'price' => 'Price',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
"title" => "Katalog",
|
||||
'categories' => 'Kategori',
|
||||
'genders' => 'Gender',
|
||||
'price' => 'Harga',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
aria-label="Add to Wishlist">
|
||||
<i class="ci-heart animate-target"></i>
|
||||
</button>
|
||||
<a class="d-flex bg-body-tertiary rounded p-3" href="{{ route('product.detail', $product->slug ?? '0') }}">
|
||||
<a class="d-flex bg-body-tertiary rounded p-3" href="{{ route('product.detail', $product->slug ?? $product->id ?? '0') }}">
|
||||
<div class="ratio" style="--cz-aspect-ratio: calc(308 / 274 * 100%)">
|
||||
<img src="{{ $product->image_url }}" loading="lazy" class="w-100 h-100 object-cover">
|
||||
</div>
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div class="nav mb-2">
|
||||
<a class="nav-link animate-target min-w-0 text-dark-emphasis p-0"
|
||||
href="{{ route('product.detail', $product->slug ?? '0') }}">
|
||||
href="{{ route('product.detail', $product->slug ?? $product->id ?? '0') }}">
|
||||
<span class="text-truncate">{{ $product->name ?? '' }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
@extends('layouts.landing', ['title' => 'Fashion Store - Catalog'])
|
||||
|
||||
@section('content')
|
||||
|
||||
|
||||
<x-layout.header />
|
||||
|
||||
<main class="content-wrapper">
|
||||
|
|
@ -11,13 +9,13 @@
|
|||
<nav class="container pt-2 pt-xxl-3 my-3 my-md-4" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('second', ['home', 'fashion-v1']) }}">Home</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Catalog with sidebar filters</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ __('catalog_fashion.title') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Page title -->
|
||||
<h1 class="h3 container pb-3 pb-lg-4">Shop catalog</h1>
|
||||
<h1 class="h3 container pb-3 pb-lg-4">{{ __('catalog_fashion.title') }}</h1>
|
||||
|
||||
|
||||
<!-- Products grid + Sidebar with filters -->
|
||||
|
|
@ -34,16 +32,15 @@
|
|||
</div>
|
||||
|
||||
<div class="offcanvas-body flex-column pt-2 py-lg-0 filter-sidebar">
|
||||
|
||||
|
||||
|
||||
<div class="accordion">
|
||||
|
||||
<!-- Genders -->
|
||||
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
||||
<h4 class="accordion-header" id="headingGenders">
|
||||
<button type="button" class="accordion-button p-0 pb-3"
|
||||
data-bs-toggle="collapse" data-bs-target="#genders" aria-expanded="true"
|
||||
aria-controls="genders">
|
||||
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||
data-bs-target="#genders" aria-expanded="true" aria-controls="genders">
|
||||
{{ __('catalog_fashion.genders') }}
|
||||
</button>
|
||||
</h4>
|
||||
|
|
@ -61,9 +58,8 @@
|
|||
<!-- Categories -->
|
||||
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
||||
<h4 class="accordion-header" id="headingCategories">
|
||||
<button type="button" class="accordion-button p-0 pb-3"
|
||||
data-bs-toggle="collapse" data-bs-target="#categories" aria-expanded="true"
|
||||
aria-controls="categories">
|
||||
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||
data-bs-target="#categories" aria-expanded="true" aria-controls="categories">
|
||||
{{ __('catalog_fashion.categories') }}
|
||||
</button>
|
||||
</h4>
|
||||
|
|
@ -82,9 +78,8 @@
|
|||
<!-- Price -->
|
||||
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
||||
<h4 class="accordion-header" id="headingPrice">
|
||||
<button type="button" class="accordion-button p-0 pb-3"
|
||||
data-bs-toggle="collapse" data-bs-target="#price" aria-expanded="true"
|
||||
aria-controls="price">
|
||||
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||
data-bs-target="#price" aria-expanded="true" aria-controls="price">
|
||||
Price
|
||||
</button>
|
||||
</h4>
|
||||
|
|
@ -97,15 +92,15 @@
|
|||
<div class="range-slider-ui"></div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="position-relative w-50">
|
||||
|
||||
<input type="number" class="form-control"
|
||||
min="0" data-range-slider-min>
|
||||
|
||||
<input type="number" class="form-control" min="0"
|
||||
data-range-slider-min>
|
||||
</div>
|
||||
<i class="ci-minus text-body-emphasis mx-2"></i>
|
||||
<div class="position-relative w-50">
|
||||
|
||||
<input type="number" class="form-control"
|
||||
min="0" data-range-slider-max>
|
||||
|
||||
<input type="number" class="form-control" min="0"
|
||||
data-range-slider-max>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -396,11 +391,10 @@
|
|||
</div> --}}
|
||||
|
||||
<!-- Status -->
|
||||
<div class="accordion-item border-0">
|
||||
{{-- <div class="accordion-item border-0">
|
||||
<h4 class="accordion-header" id="headingStatus">
|
||||
<button type="button" class="accordion-button p-0 pb-3"
|
||||
data-bs-toggle="collapse" data-bs-target="#status" aria-expanded="true"
|
||||
aria-controls="status">
|
||||
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||
data-bs-target="#status" aria-expanded="true" aria-controls="status">
|
||||
Status
|
||||
</button>
|
||||
</h4>
|
||||
|
|
@ -427,7 +421,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -439,7 +433,8 @@
|
|||
|
||||
<!-- Sorting -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mt-n2 mb-3 mb-sm-4">
|
||||
<div class="fs-sm text-body-emphasis text-nowrap">Found <span class="fw-semibold" id="product-count">Loading...</span>
|
||||
<div class="fs-sm text-body-emphasis text-nowrap">Found <span class="fw-semibold"
|
||||
id="product-count">Loading...</span>
|
||||
items
|
||||
</div>
|
||||
<div class="d-flex align-items-center text-nowrap">
|
||||
|
|
@ -452,12 +447,20 @@
|
|||
"containerInner": ["form-select", "border-0", "rounded-0", "px-1"]
|
||||
}
|
||||
}'
|
||||
onchange="window.location.href='{{ route('product.index') }}?sort_by='+this.value+'&{{ http_build_query(request()->except('sort_by')) }}'">
|
||||
<option value="relevance" {{ request('sort_by') == 'relevance' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_relevance') }}</option>
|
||||
<option value="popularity" {{ request('sort_by') == 'popularity' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_popularity') }}</option>
|
||||
<option value="price_low_to_high" {{ request('sort_by') == 'price_low_to_high' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_price_low_to_high') }}</option>
|
||||
<option value="price_high_to_low" {{ request('sort_by') == 'price_high_to_low' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_price_high_to_low') }}</option>
|
||||
<option value="new" {{ request('sort_by') == 'newest_arrivals' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_newest_arrivals') }}</option>
|
||||
onchange="window.location.href='{{ route('product.index') }}?sort_by='+this.value+'&{{ http_build_query(request()->except('sort_by')) }}'">
|
||||
<option value="relevance" {{ request('sort_by') == 'relevance' ? 'selected' : '' }}>
|
||||
{{ __('catalog_fashion.sort_relevance') }}</option>
|
||||
<option value="popularity" {{ request('sort_by') == 'popularity' ? 'selected' : '' }}>
|
||||
{{ __('catalog_fashion.sort_popularity') }}</option>
|
||||
<option value="price_low_to_high"
|
||||
{{ request('sort_by') == 'price_low_to_high' ? 'selected' : '' }}>
|
||||
{{ __('catalog_fashion.sort_price_low_to_high') }}</option>
|
||||
<option value="price_high_to_low"
|
||||
{{ request('sort_by') == 'price_high_to_low' ? 'selected' : '' }}>
|
||||
{{ __('catalog_fashion.sort_price_high_to_low') }}</option>
|
||||
<option value="new"
|
||||
{{ request('sort_by') == 'newest_arrivals' ? 'selected' : '' }}>
|
||||
{{ __('catalog_fashion.sort_newest_arrivals') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -466,9 +469,9 @@
|
|||
<div class="row gy-4 gy-md-5 pb-4 pb-md-5" id="products-container">
|
||||
<!-- Products will be loaded here via AJAX -->
|
||||
</div>
|
||||
{{-- <div class="col-12 col-md-8 mb-2 mb-sm-3 mb-md-0">
|
||||
{{-- <div class="col-12 col-md-8 mb-2 mb-sm-3 mb-md-0">
|
||||
<div
|
||||
class="position-relative bg-body-tertiary text-center rounded-4 p-4 p-sm-5 py-md-4 py-xl-5">
|
||||
class="position-relative text-center rounded-4 p-4 p-sm-5 py-md-4 py-xl-5">
|
||||
<p class="fs-xs text-body-secondary mb-1">Sweatshirts</p>
|
||||
<h2 class="h4 mb-4">Colors for your mood</h2>
|
||||
<div class="swiper user-select-none mb-4"
|
||||
|
|
@ -506,15 +509,15 @@
|
|||
</div>
|
||||
</div> --}}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Show more button -->
|
||||
<a href="{{ route('product.index', array_merge(request()->except('page'), ['page' => 1])) }}" type="button" class="btn btn-lg btn-outline-secondary w-100">
|
||||
Show more
|
||||
<i class="ci-chevron-down fs-xl ms-2 me-n1"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Show more button -->
|
||||
<button type="button" class="btn btn-lg btn-outline-secondary w-100" id="show-more-btn">
|
||||
Show more
|
||||
<i class="ci-chevron-down fs-xl ms-2 me-n1"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -595,200 +598,428 @@
|
|||
Filters
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.shimmer-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shimmer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
rgba(255, 255, 255, 0) 100%);
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.shimmer-line {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.shimmer-content {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadGenders();
|
||||
loadCategories();
|
||||
loadProducts();
|
||||
|
||||
// Handle sort change
|
||||
const sortSelect = document.querySelector('select[onchange*="sort_by"]');
|
||||
if (sortSelect) {
|
||||
sortSelect.removeAttribute('onchange');
|
||||
sortSelect.addEventListener('change', function() {
|
||||
loadProducts({ sort_by: this.value });
|
||||
});
|
||||
}
|
||||
|
||||
// Handle price range changes
|
||||
const priceInputs = document.querySelectorAll('[data-range-slider-min], [data-range-slider-max]');
|
||||
priceInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
const minVal = document.querySelector('[data-range-slider-min]').value;
|
||||
const maxVal = document.querySelector('[data-range-slider-max]').value;
|
||||
loadProducts({
|
||||
price_range_start: minVal || null,
|
||||
price_range_end: maxVal || null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Attach initial filter event listeners
|
||||
attachFilterEventListeners();
|
||||
});
|
||||
|
||||
function removeFilter(filterKey) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.delete(`filter[${filterKey}]`);
|
||||
|
||||
// Convert URLSearchParams to object for loadProducts
|
||||
const params = {};
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
loadProducts(params);
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Remove all filter parameters
|
||||
for (const [key] of urlParams.entries()) {
|
||||
if (key.startsWith('filter[')) {
|
||||
urlParams.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep non-filter parameters
|
||||
const params = {};
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
if (!key.startsWith('filter[')) {
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadProducts(params);
|
||||
}
|
||||
|
||||
function loadGenders() {
|
||||
const currentGenderId = new URLSearchParams(window.location.search).get('filter[gender]');
|
||||
|
||||
fetch(`{{ route('product.ajax.genders') }}?current_gender=${currentGenderId || ''}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('genders-list').innerHTML = data.genders;
|
||||
|
||||
// Attach event listeners to newly loaded gender links
|
||||
const genderLinks = document.querySelectorAll('#genders-list a[data-gender-id]');
|
||||
genderLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Remove active class from all gender links
|
||||
genderLinks.forEach(g => g.classList.remove('active', 'text-primary'));
|
||||
// Add active class to clicked link
|
||||
this.classList.add('active', 'text-primary');
|
||||
|
||||
const genderId = this.getAttribute('data-gender-id');
|
||||
loadProducts({ 'filter[gender]': genderId });
|
||||
// Loading state lock
|
||||
let isLoading = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadGenders();
|
||||
loadCategories();
|
||||
loadProducts();
|
||||
|
||||
// Handle sort change
|
||||
const sortSelect = document.querySelector('select[onchange*="sort_by"]');
|
||||
if (sortSelect) {
|
||||
sortSelect.removeAttribute('onchange');
|
||||
sortSelect.addEventListener('change', function() {
|
||||
loadProducts({
|
||||
sort_by: this.value
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading genders:', error);
|
||||
document.getElementById('genders-list').innerHTML = '<li class="nav-item mb-1"><span class="text-danger">Error loading genders</span></li>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadCategories() {
|
||||
const currentCategoryId = new URLSearchParams(window.location.search).get('filter[category]');
|
||||
|
||||
fetch(`{{ route('product.ajax.categories') }}?current_category=${currentCategoryId || ''}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
|
||||
// Handle price range changes
|
||||
const priceInputs = document.querySelectorAll('[data-range-slider-min], [data-range-slider-max]');
|
||||
priceInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
debouncePriceSlider(function() {
|
||||
const minVal = document.querySelector('[data-range-slider-min]').value;
|
||||
const maxVal = document.querySelector('[data-range-slider-max]').value;
|
||||
loadProducts({
|
||||
price_range_start: minVal || null,
|
||||
price_range_end: maxVal || null
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Debounce function for price slider
|
||||
let priceSliderTimeout;
|
||||
function debouncePriceSlider(callback, delay = 300) {
|
||||
clearTimeout(priceSliderTimeout);
|
||||
priceSliderTimeout = setTimeout(callback, delay);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('categories-list').innerHTML = data.categories;
|
||||
|
||||
// Handle range slider UI changes
|
||||
const rangeSliderUI = document.querySelector('.range-slider-ui');
|
||||
if (rangeSliderUI) {
|
||||
// Use MutationObserver to detect slider changes
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' || mutation.type === 'attributes') {
|
||||
debouncePriceSlider(function() {
|
||||
const minVal = document.querySelector('[data-range-slider-min]').value;
|
||||
const maxVal = document.querySelector('[data-range-slider-max]').value;
|
||||
loadProducts({
|
||||
price_range_start: minVal || null,
|
||||
price_range_end: maxVal || null
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Attach event listeners to newly loaded category links
|
||||
const categoryLinks = document.querySelectorAll('#categories-list a[data-category-id]');
|
||||
categoryLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Remove active class from all category links
|
||||
categoryLinks.forEach(c => c.classList.remove('active', 'text-primary'));
|
||||
// Add active class to clicked link
|
||||
this.classList.add('active', 'text-primary');
|
||||
|
||||
const categoryId = this.getAttribute('data-category-id');
|
||||
loadProducts({ 'filter[category]': categoryId });
|
||||
observer.observe(rangeSliderUI, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['style', 'class']
|
||||
});
|
||||
|
||||
// Also handle mouseup/touchend events on the slider
|
||||
rangeSliderUI.addEventListener('mouseup', function() {
|
||||
debouncePriceSlider(function() {
|
||||
const minVal = document.querySelector('[data-range-slider-min]').value;
|
||||
const maxVal = document.querySelector('[data-range-slider-max]').value;
|
||||
loadProducts({
|
||||
price_range_start: minVal || null,
|
||||
price_range_end: maxVal || null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
rangeSliderUI.addEventListener('touchend', function() {
|
||||
debouncePriceSlider(function() {
|
||||
const minVal = document.querySelector('[data-range-slider-min]').value;
|
||||
const maxVal = document.querySelector('[data-range-slider-max]').value;
|
||||
loadProducts({
|
||||
price_range_start: minVal || null,
|
||||
price_range_end: maxVal || null
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading categories:', error);
|
||||
document.getElementById('categories-list').innerHTML = '<li class="nav-item mb-1"><span class="text-danger">Error loading categories</span></li>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadProducts(params = {}) {
|
||||
const container = document.getElementById('products-container');
|
||||
const countElement = document.getElementById('product-count');
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="col-12 text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
||||
countElement.textContent = 'Loading...';
|
||||
|
||||
// Get current URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Add custom parameters
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] !== null) {
|
||||
urlParams.set(key, params[key]);
|
||||
|
||||
// Attach initial filter event listeners
|
||||
attachFilterEventListeners();
|
||||
|
||||
// Handle show more button
|
||||
const showMoreBtn = document.getElementById('show-more-btn');
|
||||
if (showMoreBtn) {
|
||||
showMoreBtn.addEventListener('click', function() {
|
||||
loadMoreProducts();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Make AJAX request
|
||||
fetch(`{{ route('product.ajax') }}?${urlParams.toString()}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
|
||||
function loadMoreProducts() {
|
||||
const container = document.getElementById('products-container');
|
||||
const showMoreBtn = document.getElementById('show-more-btn');
|
||||
|
||||
// Get current page from URL or default to 2 (next page)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentPage = parseInt(urlParams.get('page')) || 1;
|
||||
const nextPage = currentPage + 1;
|
||||
|
||||
// Show loading state
|
||||
showMoreBtn.innerHTML =
|
||||
'<div class="spinner-border spinner-border-sm me-2" role="status"><span class="visually-hidden">Loading...</span></div> Loading...';
|
||||
showMoreBtn.disabled = true;
|
||||
|
||||
// Set page parameter and load products
|
||||
urlParams.set('page', nextPage);
|
||||
|
||||
fetch(`{{ route('product.ajax') }}?${urlParams.toString()}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Append new products to existing container
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = data.products;
|
||||
|
||||
// Append each new product
|
||||
while (tempDiv.firstChild) {
|
||||
container.appendChild(tempDiv.firstChild);
|
||||
}
|
||||
|
||||
// Update show more button
|
||||
if (data.has_more) {
|
||||
showMoreBtn.innerHTML = 'Show more <i class="ci-chevron-down fs-xl ms-2 me-n1"></i>';
|
||||
showMoreBtn.disabled = false;
|
||||
} else {
|
||||
showMoreBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update URL
|
||||
window.history.pushState({
|
||||
path: window.location.pathname + '?' + urlParams.toString()
|
||||
}, '', window.location.pathname + '?' + urlParams.toString());
|
||||
} else {
|
||||
showMoreBtn.innerHTML = 'Show more <i class="ci-chevron-down fs-xl ms-2 me-n1"></i>';
|
||||
showMoreBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading more products:', error);
|
||||
showMoreBtn.innerHTML = 'Show more <i class="ci-chevron-down fs-xl ms-2 me-n1"></i>';
|
||||
showMoreBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function removeFilter(filterKey) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.delete(`filter[${filterKey}]`);
|
||||
|
||||
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
|
||||
history.replaceState(null, '', newUrl);
|
||||
|
||||
// remove button
|
||||
const button = document.querySelector(`.remove-filter[data-filter-key="${filterKey}"]`);
|
||||
if (button) {
|
||||
button.remove();
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
container.innerHTML = data.products;
|
||||
countElement.textContent = data.count;
|
||||
|
||||
// Update filters section
|
||||
updateFiltersSection(data.filters);
|
||||
|
||||
// Update URL without page reload
|
||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
} else {
|
||||
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Error loading products</div>';
|
||||
countElement.textContent = '0';
|
||||
|
||||
loadGenders();
|
||||
loadCategories();
|
||||
|
||||
loadProducts(Object.fromEntries(urlParams.entries()));
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Collect all filter keys first
|
||||
const filterKeys = [];
|
||||
for (const [key] of urlParams.entries()) {
|
||||
if (key.startsWith('filter[')) {
|
||||
filterKeys.push(key);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Error loading products</div>';
|
||||
countElement.textContent = '0';
|
||||
});
|
||||
}
|
||||
|
||||
function updateFiltersSection(filters) {
|
||||
const filtersContainer = document.querySelector('.filter-sidebar');
|
||||
const filtersSection = filtersContainer.querySelector('.pb-4.mb-2.mb-xl-3');
|
||||
|
||||
if (Object.keys(filters).length > 0) {
|
||||
// Build filters HTML
|
||||
let filtersHtml = `
|
||||
|
||||
// Delete all filter keys
|
||||
filterKeys.forEach(key => urlParams.delete(key));
|
||||
|
||||
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
|
||||
history.replaceState(null, '', newUrl);
|
||||
|
||||
// remove all button
|
||||
const buttons = document.querySelectorAll('.remove-filter');
|
||||
buttons.forEach(button => {
|
||||
button.remove();
|
||||
});
|
||||
|
||||
loadGenders();
|
||||
loadCategories();
|
||||
|
||||
loadProducts(Object.fromEntries(urlParams.entries()));
|
||||
}
|
||||
|
||||
function loadGenders() {
|
||||
const currentGenderId = new URLSearchParams(window.location.search).get('filter[gender]');
|
||||
|
||||
fetch(`{{ route('product.ajax.genders') }}?current_gender=${currentGenderId || ''}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('genders-list').innerHTML = data.genders;
|
||||
|
||||
// Attach event listeners to newly loaded gender links
|
||||
const genderLinks = document.querySelectorAll('#genders-list a[data-gender-id]');
|
||||
genderLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Remove active class from all gender links
|
||||
genderLinks.forEach(g => g.classList.remove('active', 'text-primary'));
|
||||
// Add active class to clicked link
|
||||
this.classList.add('active', 'text-primary');
|
||||
|
||||
const genderId = this.getAttribute('data-gender-id');
|
||||
loadProducts({
|
||||
'filter[gender]': genderId
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading genders:', error);
|
||||
document.getElementById('genders-list').innerHTML =
|
||||
'<li class="nav-item mb-1"><span class="text-danger">Error loading genders</span></li>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadCategories() {
|
||||
const currentCategoryId = new URLSearchParams(window.location.search).get('filter[category]');
|
||||
|
||||
fetch(`{{ route('product.ajax.categories') }}?current_category=${currentCategoryId || ''}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('categories-list').innerHTML = data.categories;
|
||||
|
||||
// Attach event listeners to newly loaded category links
|
||||
const categoryLinks = document.querySelectorAll('#categories-list a[data-category-id]');
|
||||
categoryLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Remove active class from all category links
|
||||
categoryLinks.forEach(c => c.classList.remove('active',
|
||||
'text-primary'));
|
||||
// Add active class to clicked link
|
||||
this.classList.add('active', 'text-primary');
|
||||
|
||||
const categoryId = this.getAttribute('data-category-id');
|
||||
loadProducts({
|
||||
'filter[category]': categoryId
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading categories:', error);
|
||||
document.getElementById('categories-list').innerHTML =
|
||||
'<li class="nav-item mb-1"><span class="text-danger">Error loading categories</span></li>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadProducts(params = {}) {
|
||||
// Prevent multiple simultaneous calls
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
const container = document.getElementById('products-container');
|
||||
const countElement = document.getElementById('product-count');
|
||||
|
||||
const shimmerItem = `<div class="col-6 col-md-4 mb-2 mb-sm-3 mb-md-0">
|
||||
<div class="shimmer-wrapper">
|
||||
<div class="shimmer">
|
||||
<div class="shimmer-content rounded">
|
||||
<div class="shimmer-line shimmer-image rounded mb-3" style="height: 0; padding-bottom: 100%; position: relative;">
|
||||
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.1); border-radius: 4px;"></div>
|
||||
</div>
|
||||
<div class="shimmer-line shimmer-title rounded mb-2" style="height: 20px; width: 80%;"></div>
|
||||
<div class="shimmer-line shimmer-price rounded" style="height: 16px; width: 60%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
// Show loading state
|
||||
container.innerHTML = shimmerItem.repeat(12);
|
||||
countElement.textContent = 'Loading...';
|
||||
|
||||
// Get current URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Add custom parameters
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] !== null) {
|
||||
urlParams.set(key, params[key]);
|
||||
}
|
||||
});
|
||||
|
||||
// Make AJAX request
|
||||
fetch(`{{ route('product.ajax') }}?${urlParams.toString()}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
container.innerHTML = data.products;
|
||||
countElement.textContent = data.count;
|
||||
|
||||
// Update filters section
|
||||
updateFiltersSection(data.filters);
|
||||
|
||||
// Update URL without page reload
|
||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
||||
window.history.pushState({
|
||||
path: newUrl
|
||||
}, '', newUrl);
|
||||
} else {
|
||||
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Error loading products</div>';
|
||||
countElement.textContent = '0';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Error loading products</div>';
|
||||
countElement.textContent = '0';
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateFiltersSection(filters) {
|
||||
const filtersContainer = document.querySelector('.filter-sidebar');
|
||||
const filtersSection = filtersContainer.querySelector('.pb-4.mb-2.mb-xl-3');
|
||||
|
||||
if (Object.keys(filters).length > 0) {
|
||||
// Build filters HTML
|
||||
let filtersHtml = `
|
||||
<div class="pb-4 mb-2 mb-xl-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="h6 mb-0">Filter</h4>
|
||||
|
|
@ -799,60 +1030,57 @@
|
|||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
`;
|
||||
|
||||
Object.keys(filters).forEach(key => {
|
||||
filtersHtml += `
|
||||
|
||||
Object.keys(filters).forEach(key => {
|
||||
filtersHtml += `
|
||||
<button type="button" class="btn btn-sm btn-secondary remove-filter"
|
||||
data-filter-key="${key}">
|
||||
<i class="ci-close fs-sm ms-n1 me-1"></i>
|
||||
${filters[key]}
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
filtersHtml += `
|
||||
});
|
||||
|
||||
filtersHtml += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update or create filters section
|
||||
if (filtersSection) {
|
||||
filtersSection.outerHTML = filtersHtml;
|
||||
|
||||
// Update or create filters section
|
||||
if (filtersSection) {
|
||||
filtersSection.outerHTML = filtersHtml;
|
||||
} else {
|
||||
filtersContainer.insertAdjacentHTML('afterbegin', filtersHtml);
|
||||
}
|
||||
|
||||
// Re-attach event listeners for new filter buttons
|
||||
attachFilterEventListeners();
|
||||
} else {
|
||||
filtersContainer.insertAdjacentHTML('afterbegin', filtersHtml);
|
||||
}
|
||||
|
||||
// Re-attach event listeners for new filter buttons
|
||||
attachFilterEventListeners();
|
||||
} else {
|
||||
// Remove filters section if no filters
|
||||
if (filtersSection) {
|
||||
filtersSection.remove();
|
||||
// Remove filters section if no filters
|
||||
if (filtersSection) {
|
||||
filtersSection.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachFilterEventListeners() {
|
||||
// Handle filter removal
|
||||
const removeFilterButtons = document.querySelectorAll('.remove-filter');
|
||||
removeFilterButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const filterKey = this.getAttribute('data-filter-key');
|
||||
removeFilter(filterKey);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle clear all filters
|
||||
const clearAllButton = document.querySelector('.clear-all-filters');
|
||||
if (clearAllButton) {
|
||||
clearAllButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
clearAllFilters();
|
||||
|
||||
function attachFilterEventListeners() {
|
||||
// Handle filter removal
|
||||
const removeFilterButtons = document.querySelectorAll('.remove-filter');
|
||||
removeFilterButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const filterKey = this.getAttribute('data-filter-key');
|
||||
removeFilter(filterKey);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle clear all filters
|
||||
const clearAllButton = document.querySelector('.clear-all-filters');
|
||||
if (clearAllButton) {
|
||||
clearAllButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
clearAllFilters();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Reference in New Issue