filter with ajax
This commit is contained in:
parent
80f6dc8612
commit
c71b7ff5c2
|
|
@ -66,6 +66,7 @@ class ProductController extends Controller
|
||||||
$page = $request->page ?? 1;
|
$page = $request->page ?? 1;
|
||||||
|
|
||||||
$search = $request->search;
|
$search = $request->search;
|
||||||
|
|
||||||
$filter = $request->filter ?? [];
|
$filter = $request->filter ?? [];
|
||||||
$sortBy = $request->sort_by ?? 'relevance';
|
$sortBy = $request->sort_by ?? 'relevance';
|
||||||
|
|
||||||
|
|
@ -102,15 +103,23 @@ class ProductController extends Controller
|
||||||
'price_range_end' => $price_range_end,
|
'price_range_end' => $price_range_end,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Check if there are more products
|
||||||
|
$hasMore = count($products) >= $limit;
|
||||||
|
|
||||||
// Render product cards HTML
|
// Render product cards HTML
|
||||||
$productHtml = '';
|
$productHtml = '';
|
||||||
foreach ($products as $product) {
|
if (count($products) == 0) {
|
||||||
$productHtml .= '<div class="col-6 col-md-4 mb-2 mb-sm-3 mb-md-0">';
|
$productHtml = '<div class="col-12">';
|
||||||
$productHtml .= view('components.home.product-card', ['product' => $product])->render();
|
$productHtml .= 'Pencarian tidak ditemukan';
|
||||||
$productHtml .= '</div>';
|
$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
|
||||||
$filter = $request->filter ?? [];
|
$filter = $request->filter ?? [];
|
||||||
if (isset($filter['category']) && $filter['category']){
|
if (isset($filter['category']) && $filter['category']){
|
||||||
|
|
@ -123,7 +132,6 @@ class ProductController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isset($filter['gender']) && $filter['gender']) {
|
if (isset($filter['gender']) && $filter['gender']) {
|
||||||
$gender = Gender::find($filter['gender']);
|
$gender = Gender::find($filter['gender']);
|
||||||
|
|
||||||
|
|
@ -141,13 +149,13 @@ class ProductController extends Controller
|
||||||
'filters' => $filters,
|
'filters' => $filters,
|
||||||
'products' => $productHtml,
|
'products' => $productHtml,
|
||||||
'count' => count($products),
|
'count' => count($products),
|
||||||
'has_more' => count($products) >= $limit
|
'has_more' => $hasMore,
|
||||||
|
'current_page' => $page
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
$productRepository = new ProductRepository;
|
$productRepository = new ProductRepository;
|
||||||
$products = [];
|
$products = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,83 +6,57 @@ use App\Models\DiscountItem;
|
||||||
use App\Models\Items;
|
use App\Models\Items;
|
||||||
use App\Models\StoreCategoryMap;
|
use App\Models\StoreCategoryMap;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Carbon\Carbon;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ProductRepository
|
class ProductRepository
|
||||||
{
|
{
|
||||||
public function getList($params)
|
public function getList($params)
|
||||||
{
|
{
|
||||||
$event = @$params["event"];
|
$event = @$params['event'];
|
||||||
$sort = @$params["sort"];
|
$sort = @$params['sort'];
|
||||||
$search = @$params["search"];
|
$search = @$params['search'];
|
||||||
$category_id = @$params["category_id"];
|
$category_id = @$params['category_id'];
|
||||||
$brand_id = @$params["brand_id"];
|
$brand_id = @$params['brand_id'];
|
||||||
$gender_id = @$params["gender_id"];
|
$gender_id = @$params['gender_id'];
|
||||||
$limit = @$params["limit"];
|
$limit = @$params['limit'];
|
||||||
|
|
||||||
$location_id = @$params["location_id"];
|
$location_id = @$params['location_id'];
|
||||||
$is_consignment = @$params["is_consignment"];
|
$is_consignment = @$params['is_consignment'];
|
||||||
|
|
||||||
$price_range_start = @$params["price_range_start"];
|
|
||||||
$price_range_end = @$params["price_range_end"];
|
|
||||||
|
|
||||||
|
$price_range_start = @$params['price_range_start'];
|
||||||
|
$price_range_end = @$params['price_range_end'];
|
||||||
|
|
||||||
$sorting_ids = [];
|
$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) {
|
if ($sort == 'price_low_to_high' || $sort == 'price_high_to_low') {
|
||||||
$query->orWhere('discounts.location_id', $location_id);
|
$params_filter = [];
|
||||||
}
|
$params_filter['location_id'] = $location_id;
|
||||||
});
|
$params_filter['sort'] = $sort;
|
||||||
})
|
$sorting_ids = collect($this->getProductList($params_filter))->pluck('id')->toArray();
|
||||||
->orderBy('discount_items.price', $sort == "price_low_to_high" ? 'asc' : 'desc')
|
|
||||||
->pluck('item_reference_id')->toArray();
|
// Log::info($sorting_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
$where_ids = [];
|
$where_ids = [];
|
||||||
if ($price_range_start && $price_range_end) {
|
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) {
|
$params_filter = [];
|
||||||
$query->where('type', 'price')
|
$params_filter['location_id'] = $location_id;
|
||||||
->where(function ($query) {
|
$params_filter['price_range_start'] = $price_range_start;
|
||||||
$query->where('valid_at', '<=', now())
|
$params_filter['price_range_end'] = $price_range_end;
|
||||||
->orWhereNull('valid_at');
|
|
||||||
})
|
$where_ids = collect($this->getProductList($params_filter))->pluck('id')->toArray();
|
||||||
->where(function ($query) {
|
|
||||||
$query->where('expired_at', '>', now())
|
if (! empty($sorting_ids) && count($sorting_ids) > 0) {
|
||||||
->orWhereNull('expired_at');
|
$where_ids = array_values(
|
||||||
})
|
array_intersect($sorting_ids, $where_ids)
|
||||||
->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);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
->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('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
|
case when discounts.valid_at is null then 'special-offer' else 'limited-sale' end as event
|
||||||
from discount_items
|
from discount_items
|
||||||
left join item_reference on item_reference_id = item_reference.id
|
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.valid_at <= now()) and
|
||||||
( discounts.valid_at is null or discounts.expired_at >= now())
|
( discounts.valid_at is null or discounts.expired_at >= now())
|
||||||
order by item_id, discounts.created_at desc
|
order by item_id, discounts.created_at desc
|
||||||
) as d"),"d.item_id","=","items.id")
|
) as d"), 'd.item_id', '=', 'items.id')
|
||||||
->when(true, function($query) use ($event, $sort,$sorting_ids){
|
->when(true, function ($query) use ($event, $sort, $sorting_ids) {
|
||||||
if ($event){
|
if ($event) {
|
||||||
$query->orderByRaw("case when brand = 'CHAMELO' then 1 else 2 end ASC ");
|
$query->orderByRaw("case when brand = 'CHAMELO' then 1 else 2 end ASC ");
|
||||||
|
|
||||||
$query->orderBy("percent", "desc");
|
$query->orderBy('percent', 'desc');
|
||||||
}else{
|
} else {
|
||||||
|
|
||||||
/* if ($event == "special-offer"){
|
if (! empty($sorting_ids) && count($sorting_ids) > 0) {
|
||||||
$query->orderByRaw("case when brand = 'CHAMELO' then 1 else 2 end ASC ");
|
|
||||||
} */
|
|
||||||
|
|
||||||
if (!empty($sorting_ids)) {
|
$caseStatements = [];
|
||||||
$ids = implode(',', $sorting_ids);
|
foreach ($sorting_ids as $index => $id) {
|
||||||
$query->orderByRaw("array_position(ARRAY[$ids], items.id)");
|
$caseStatements[] = "WHEN items.id = {$id} THEN " . ($index + 1);
|
||||||
}else if ($sort == "new"){
|
}
|
||||||
$query->orderByRaw("case when category1 in ('CLUBS','CLUB','COMPONENT HEAD') and brand = 'PXG' then 1 else 2 end ASC");
|
$caseStatements[] = "ELSE 999";
|
||||||
} 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");
|
$query->orderByRaw("
|
||||||
}
|
CASE
|
||||||
|
" . implode("\n ", $caseStatements) . "
|
||||||
|
END ASC
|
||||||
|
");
|
||||||
|
|
||||||
|
} 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()');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
->where("is_publish", true)
|
->where('is_publish', true)
|
||||||
->when($search, function ($query) use ($search) {
|
->when($search, function ($query) use ($search) {
|
||||||
$query->where(function ($query) use ($search) {
|
$query->where(function ($query) use ($search) {
|
||||||
$query->where('number', 'ILIKE', "%$search%")
|
$query->where('number', 'ILIKE', "%$search%")
|
||||||
|
|
@ -130,8 +110,11 @@ class ProductRepository
|
||||||
->when($gender_id, function ($query) use ($gender_id) {
|
->when($gender_id, function ($query) use ($gender_id) {
|
||||||
$query->where('gender_id', $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);
|
$query->where('d.event', $event);
|
||||||
|
})
|
||||||
|
->when($where_ids, function ($query) use ($where_ids) {
|
||||||
|
$query->whereIn('items.id', $where_ids);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($category_id) {
|
if ($category_id) {
|
||||||
|
|
@ -155,10 +138,128 @@ class ProductRepository
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return $builder->paginate($limit);
|
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()
|
public function getMinMaxPrice()
|
||||||
{
|
{
|
||||||
|
|
@ -168,13 +269,13 @@ class ProductRepository
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function show($slug)
|
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) {
|
if ($product == null) {
|
||||||
abort(404);
|
abort(404);
|
||||||
|
|
@ -195,7 +296,7 @@ class ProductRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove except_ids from the list
|
// 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();
|
return Items::whereIn('id', $ids)->inRandomOrder()->take(10)->get();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
"title" => "Catalog",
|
||||||
'categories' => 'Categories',
|
'categories' => 'Categories',
|
||||||
'genders' => 'Genders',
|
'genders' => 'Genders',
|
||||||
'price' => 'Price',
|
'price' => 'Price',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
"title" => "Katalog",
|
||||||
'categories' => 'Kategori',
|
'categories' => 'Kategori',
|
||||||
'genders' => 'Gender',
|
'genders' => 'Gender',
|
||||||
'price' => 'Harga',
|
'price' => 'Harga',
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
aria-label="Add to Wishlist">
|
aria-label="Add to Wishlist">
|
||||||
<i class="ci-heart animate-target"></i>
|
<i class="ci-heart animate-target"></i>
|
||||||
</button>
|
</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%)">
|
<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">
|
<img src="{{ $product->image_url }}" loading="lazy" class="w-100 h-100 object-cover">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="nav mb-2">
|
<div class="nav mb-2">
|
||||||
<a class="nav-link animate-target min-w-0 text-dark-emphasis p-0"
|
<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>
|
<span class="text-truncate">{{ $product->name ?? '' }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
@extends('layouts.landing', ['title' => 'Fashion Store - Catalog'])
|
@extends('layouts.landing', ['title' => 'Fashion Store - Catalog'])
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
||||||
|
|
||||||
<x-layout.header />
|
<x-layout.header />
|
||||||
|
|
||||||
<main class="content-wrapper">
|
<main class="content-wrapper">
|
||||||
|
|
@ -11,13 +9,13 @@
|
||||||
<nav class="container pt-2 pt-xxl-3 my-3 my-md-4" aria-label="breadcrumb">
|
<nav class="container pt-2 pt-xxl-3 my-3 my-md-4" aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{{ route('second', ['home', 'fashion-v1']) }}">Home</a></li>
|
<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>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page title -->
|
<!-- 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 -->
|
<!-- Products grid + Sidebar with filters -->
|
||||||
|
|
@ -41,9 +39,8 @@
|
||||||
<!-- Genders -->
|
<!-- Genders -->
|
||||||
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
||||||
<h4 class="accordion-header" id="headingGenders">
|
<h4 class="accordion-header" id="headingGenders">
|
||||||
<button type="button" class="accordion-button p-0 pb-3"
|
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||||
data-bs-toggle="collapse" data-bs-target="#genders" aria-expanded="true"
|
data-bs-target="#genders" aria-expanded="true" aria-controls="genders">
|
||||||
aria-controls="genders">
|
|
||||||
{{ __('catalog_fashion.genders') }}
|
{{ __('catalog_fashion.genders') }}
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -61,9 +58,8 @@
|
||||||
<!-- Categories -->
|
<!-- Categories -->
|
||||||
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
||||||
<h4 class="accordion-header" id="headingCategories">
|
<h4 class="accordion-header" id="headingCategories">
|
||||||
<button type="button" class="accordion-button p-0 pb-3"
|
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||||
data-bs-toggle="collapse" data-bs-target="#categories" aria-expanded="true"
|
data-bs-target="#categories" aria-expanded="true" aria-controls="categories">
|
||||||
aria-controls="categories">
|
|
||||||
{{ __('catalog_fashion.categories') }}
|
{{ __('catalog_fashion.categories') }}
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -82,9 +78,8 @@
|
||||||
<!-- Price -->
|
<!-- Price -->
|
||||||
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
<div class="accordion-item border-0 pb-1 pb-xl-2">
|
||||||
<h4 class="accordion-header" id="headingPrice">
|
<h4 class="accordion-header" id="headingPrice">
|
||||||
<button type="button" class="accordion-button p-0 pb-3"
|
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||||
data-bs-toggle="collapse" data-bs-target="#price" aria-expanded="true"
|
data-bs-target="#price" aria-expanded="true" aria-controls="price">
|
||||||
aria-controls="price">
|
|
||||||
Price
|
Price
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -98,14 +93,14 @@
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="position-relative w-50">
|
<div class="position-relative w-50">
|
||||||
|
|
||||||
<input type="number" class="form-control"
|
<input type="number" class="form-control" min="0"
|
||||||
min="0" data-range-slider-min>
|
data-range-slider-min>
|
||||||
</div>
|
</div>
|
||||||
<i class="ci-minus text-body-emphasis mx-2"></i>
|
<i class="ci-minus text-body-emphasis mx-2"></i>
|
||||||
<div class="position-relative w-50">
|
<div class="position-relative w-50">
|
||||||
|
|
||||||
<input type="number" class="form-control"
|
<input type="number" class="form-control" min="0"
|
||||||
min="0" data-range-slider-max>
|
data-range-slider-max>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -396,11 +391,10 @@
|
||||||
</div> --}}
|
</div> --}}
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<div class="accordion-item border-0">
|
{{-- <div class="accordion-item border-0">
|
||||||
<h4 class="accordion-header" id="headingStatus">
|
<h4 class="accordion-header" id="headingStatus">
|
||||||
<button type="button" class="accordion-button p-0 pb-3"
|
<button type="button" class="accordion-button p-0 pb-3" data-bs-toggle="collapse"
|
||||||
data-bs-toggle="collapse" data-bs-target="#status" aria-expanded="true"
|
data-bs-target="#status" aria-expanded="true" aria-controls="status">
|
||||||
aria-controls="status">
|
|
||||||
Status
|
Status
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
@ -427,7 +421,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> --}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -439,7 +433,8 @@
|
||||||
|
|
||||||
<!-- Sorting -->
|
<!-- Sorting -->
|
||||||
<div class="d-sm-flex align-items-center justify-content-between mt-n2 mb-3 mb-sm-4">
|
<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
|
items
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center text-nowrap">
|
<div class="d-flex align-items-center text-nowrap">
|
||||||
|
|
@ -452,12 +447,20 @@
|
||||||
"containerInner": ["form-select", "border-0", "rounded-0", "px-1"]
|
"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')) }}'">
|
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="relevance" {{ request('sort_by') == 'relevance' ? 'selected' : '' }}>
|
||||||
<option value="popularity" {{ request('sort_by') == 'popularity' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_popularity') }}</option>
|
{{ __('catalog_fashion.sort_relevance') }}</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="popularity" {{ request('sort_by') == 'popularity' ? 'selected' : '' }}>
|
||||||
<option value="price_high_to_low" {{ request('sort_by') == 'price_high_to_low' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_price_high_to_low') }}</option>
|
{{ __('catalog_fashion.sort_popularity') }}</option>
|
||||||
<option value="new" {{ request('sort_by') == 'newest_arrivals' ? 'selected' : '' }}>{{ __('catalog_fashion.sort_newest_arrivals') }}</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -466,9 +469,9 @@
|
||||||
<div class="row gy-4 gy-md-5 pb-4 pb-md-5" id="products-container">
|
<div class="row gy-4 gy-md-5 pb-4 pb-md-5" id="products-container">
|
||||||
<!-- Products will be loaded here via AJAX -->
|
<!-- Products will be loaded here via AJAX -->
|
||||||
</div>
|
</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
|
<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>
|
<p class="fs-xs text-body-secondary mb-1">Sweatshirts</p>
|
||||||
<h2 class="h4 mb-4">Colors for your mood</h2>
|
<h2 class="h4 mb-4">Colors for your mood</h2>
|
||||||
<div class="swiper user-select-none mb-4"
|
<div class="swiper user-select-none mb-4"
|
||||||
|
|
@ -507,14 +510,14 @@
|
||||||
</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>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -595,200 +598,428 @@
|
||||||
Filters
|
Filters
|
||||||
</button>
|
</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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// Loading state lock
|
||||||
loadGenders();
|
let isLoading = false;
|
||||||
loadCategories();
|
|
||||||
loadProducts();
|
|
||||||
|
|
||||||
// Handle sort change
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const sortSelect = document.querySelector('select[onchange*="sort_by"]');
|
loadGenders();
|
||||||
if (sortSelect) {
|
loadCategories();
|
||||||
sortSelect.removeAttribute('onchange');
|
loadProducts();
|
||||||
sortSelect.addEventListener('change', function() {
|
|
||||||
loadProducts({ sort_by: this.value });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle price range changes
|
// Handle sort change
|
||||||
const priceInputs = document.querySelectorAll('[data-range-slider-min], [data-range-slider-max]');
|
const sortSelect = document.querySelector('select[onchange*="sort_by"]');
|
||||||
priceInputs.forEach(input => {
|
if (sortSelect) {
|
||||||
input.addEventListener('change', function() {
|
sortSelect.removeAttribute('onchange');
|
||||||
const minVal = document.querySelector('[data-range-slider-min]').value;
|
sortSelect.addEventListener('change', function() {
|
||||||
const maxVal = document.querySelector('[data-range-slider-max]').value;
|
loadProducts({
|
||||||
loadProducts({
|
sort_by: this.value
|
||||||
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 });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.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() {
|
// Handle price range changes
|
||||||
const currentCategoryId = new URLSearchParams(window.location.search).get('filter[category]');
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
fetch(`{{ route('product.ajax.categories') }}?current_category=${currentCategoryId || ''}`, {
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
// Debounce function for price slider
|
||||||
'Accept': 'application/json'
|
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;
|
|
||||||
|
|
||||||
// Attach event listeners to newly loaded category links
|
// Handle range slider UI changes
|
||||||
const categoryLinks = document.querySelectorAll('#categories-list a[data-category-id]');
|
const rangeSliderUI = document.querySelector('.range-slider-ui');
|
||||||
categoryLinks.forEach(link => {
|
if (rangeSliderUI) {
|
||||||
link.addEventListener('click', function(e) {
|
// Use MutationObserver to detect slider changes
|
||||||
e.preventDefault();
|
const observer = new MutationObserver(function(mutations) {
|
||||||
// Remove active class from all category links
|
mutations.forEach(function(mutation) {
|
||||||
categoryLinks.forEach(c => c.classList.remove('active', 'text-primary'));
|
if (mutation.type === 'childList' || mutation.type === 'attributes') {
|
||||||
// Add active class to clicked link
|
debouncePriceSlider(function() {
|
||||||
this.classList.add('active', 'text-primary');
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const categoryId = this.getAttribute('data-category-id');
|
observer.observe(rangeSliderUI, {
|
||||||
loadProducts({ 'filter[category]': categoryId });
|
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 = {}) {
|
// Attach initial filter event listeners
|
||||||
const container = document.getElementById('products-container');
|
attachFilterEventListeners();
|
||||||
const countElement = document.getElementById('product-count');
|
|
||||||
|
|
||||||
// Show loading state
|
// Handle show more button
|
||||||
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>';
|
const showMoreBtn = document.getElementById('show-more-btn');
|
||||||
countElement.textContent = 'Loading...';
|
if (showMoreBtn) {
|
||||||
|
showMoreBtn.addEventListener('click', function() {
|
||||||
// Get current URL parameters
|
loadMoreProducts();
|
||||||
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
|
function loadMoreProducts() {
|
||||||
fetch(`{{ route('product.ajax') }}?${urlParams.toString()}`, {
|
const container = document.getElementById('products-container');
|
||||||
headers: {
|
const showMoreBtn = document.getElementById('show-more-btn');
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
'Accept': 'application/json'
|
// 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
|
loadGenders();
|
||||||
updateFiltersSection(data.filters);
|
loadCategories();
|
||||||
|
|
||||||
// Update URL without page reload
|
loadProducts(Object.fromEntries(urlParams.entries()));
|
||||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
}
|
||||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
|
||||||
} else {
|
function clearAllFilters() {
|
||||||
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Error loading products</div>';
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
countElement.textContent = '0';
|
|
||||||
|
// 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) {
|
// Delete all filter keys
|
||||||
const filtersContainer = document.querySelector('.filter-sidebar');
|
filterKeys.forEach(key => urlParams.delete(key));
|
||||||
const filtersSection = filtersContainer.querySelector('.pb-4.mb-2.mb-xl-3');
|
|
||||||
|
|
||||||
if (Object.keys(filters).length > 0) {
|
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
|
||||||
// Build filters HTML
|
history.replaceState(null, '', newUrl);
|
||||||
let filtersHtml = `
|
|
||||||
|
// 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="pb-4 mb-2 mb-xl-3">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<h4 class="h6 mb-0">Filter</h4>
|
<h4 class="h6 mb-0">Filter</h4>
|
||||||
|
|
@ -800,59 +1031,56 @@
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
Object.keys(filters).forEach(key => {
|
Object.keys(filters).forEach(key => {
|
||||||
filtersHtml += `
|
filtersHtml += `
|
||||||
<button type="button" class="btn btn-sm btn-secondary remove-filter"
|
<button type="button" class="btn btn-sm btn-secondary remove-filter"
|
||||||
data-filter-key="${key}">
|
data-filter-key="${key}">
|
||||||
<i class="ci-close fs-sm ms-n1 me-1"></i>
|
<i class="ci-close fs-sm ms-n1 me-1"></i>
|
||||||
${filters[key]}
|
${filters[key]}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
filtersHtml += `
|
filtersHtml += `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Update or create filters section
|
// Update or create filters section
|
||||||
if (filtersSection) {
|
if (filtersSection) {
|
||||||
filtersSection.outerHTML = filtersHtml;
|
filtersSection.outerHTML = filtersHtml;
|
||||||
|
} else {
|
||||||
|
filtersContainer.insertAdjacentHTML('afterbegin', filtersHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-attach event listeners for new filter buttons
|
||||||
|
attachFilterEventListeners();
|
||||||
} else {
|
} else {
|
||||||
filtersContainer.insertAdjacentHTML('afterbegin', filtersHtml);
|
// Remove filters section if no filters
|
||||||
}
|
if (filtersSection) {
|
||||||
|
filtersSection.remove();
|
||||||
// Re-attach event listeners for new filter buttons
|
}
|
||||||
attachFilterEventListeners();
|
|
||||||
} else {
|
|
||||||
// Remove filters section if no filters
|
|
||||||
if (filtersSection) {
|
|
||||||
filtersSection.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function attachFilterEventListeners() {
|
function attachFilterEventListeners() {
|
||||||
// Handle filter removal
|
// Handle filter removal
|
||||||
const removeFilterButtons = document.querySelectorAll('.remove-filter');
|
const removeFilterButtons = document.querySelectorAll('.remove-filter');
|
||||||
removeFilterButtons.forEach(button => {
|
removeFilterButtons.forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
const filterKey = this.getAttribute('data-filter-key');
|
const filterKey = this.getAttribute('data-filter-key');
|
||||||
removeFilter(filterKey);
|
removeFilter(filterKey);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Handle clear all filters
|
// Handle clear all filters
|
||||||
const clearAllButton = document.querySelector('.clear-all-filters');
|
const clearAllButton = document.querySelector('.clear-all-filters');
|
||||||
if (clearAllButton) {
|
if (clearAllButton) {
|
||||||
clearAllButton.addEventListener('click', function(e) {
|
clearAllButton.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
clearAllFilters();
|
clearAllFilters();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('scripts')
|
|
||||||
@endsection
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue