load product ajax

This commit is contained in:
Bayu Lukman Yusuf 2026-01-07 14:01:43 +07:00
parent 8548016200
commit 80f6dc8612
3 changed files with 351 additions and 66 deletions

View File

@ -12,27 +12,66 @@ use Illuminate\Support\Facades\Cache;
class ProductController extends Controller
{
public function index(Request $request)
public function genders(Request $request)
{
$genderRepository = new GenderRepository;
$genders = $genderRepository->getList([]);
// Render gender links HTML
$genderHtml = '';
$currentGenderId = $request->input('current_gender');
foreach ($genders as $gender) {
$isActive = $currentGenderId == $gender->id;
$genderHtml .= '<li class="nav-item mb-1">';
$genderHtml .= '<a class="nav-link d-block fw-normal p-0 ' . ($isActive ? 'active text-primary' : '') . '" ';
$genderHtml .= 'href="#" data-gender-id="' . $gender->id . '">';
$genderHtml .= $gender->name;
$genderHtml .= '</a></li>';
}
return response()->json([
'success' => true,
'genders' => $genderHtml
]);
}
public function categories(Request $request)
{
$categoryRepository = new CategoryRepository;
$categories = $categoryRepository->getList([]);
// Render category links HTML
$categoryHtml = '';
$currentCategoryId = $request->input('current_category');
foreach ($categories as $category) {
$isActive = $currentCategoryId == $category->id;
$categoryHtml .= '<li class="nav-item mb-1">';
$categoryHtml .= '<a class="nav-link d-block fw-normal p-0 ' . ($isActive ? 'active text-primary' : '') . '" ';
$categoryHtml .= 'href="#" data-category-id="' . $category->id . '">';
$categoryHtml .= $category->name;
$categoryHtml .= '</a></li>';
}
return response()->json([
'success' => true,
'categories' => $categoryHtml
]);
}
public function ajax(Request $request)
{
$limit = 20;
$page = $request->page ?? 1;
$search = $request->search;
$filter = $request->filter ?? [];
$sortBy = $request->sort_by ?? 'relevance';
$price_range_start = $request->price_range_start ?? null;
$price_range_end = $request->price_range_end ?? null;
$genderRepository = new GenderRepository;
$categoryRepository = new CategoryRepository;
$genders = $genderRepository->getList([]);
$categories = $categoryRepository->getList([]);
$user = auth()->user();
$userId = $user ? $user->id : 0;
[$location_id, $is_consignment] = Cache::remember('employee_user_'.$userId, 60 * 60 * 24, function () use ($user) {
@ -63,7 +102,17 @@ class ProductController extends Controller
'price_range_end' => $price_range_end,
]);
// 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();
$productHtml .= '</div>';
}
// filter
$filter = $request->filter ?? [];
if (isset($filter['category']) && $filter['category']){
$category = StoreCategory::find($filter['category']);
@ -87,16 +136,32 @@ class ProductController extends Controller
$filters = $filter;
return response()->json([
'success' => true,
'filters' => $filters,
'products' => $productHtml,
'count' => count($products),
'has_more' => count($products) >= $limit
]);
}
public function index(Request $request)
{
$productRepository = new ProductRepository;
$products = [];
$filters = [];
$min_max_price = $productRepository->getMinMaxPrice();
return view('shop.catalog-fashion', [
'filters' => $filters,
'genders' => $genders,
'categories' => $categories,
'products' => $products,
'page' => $page,
'min_max_price' => $min_max_price,
]);
}

View File

@ -33,28 +33,8 @@
data-bs-target="#filterSidebar" aria-label="Close"></button>
</div>
<div class="offcanvas-body flex-column pt-2 py-lg-0">
@if (count($filters) > 0)
<!-- Selected filters + Sorting -->
<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>
<a type="button" href="{{ route('product.index') }}"
class="btn btn-sm btn-secondary bg-transparent border-0 text-decoration-underline p-0 ms-2">
Clear all
</a>
</div>
<div class="d-flex flex-wrap gap-2">
@foreach ($filters as $key => $filter)
<a href="{{ route('product.index',[http_build_query(request()->except('filter.'.$key))]) }}" type="button" class="btn btn-sm btn-secondary">
<i class="ci-close fs-sm ms-n1 me-1"></i>
{{ $filter }}
</a>
@endforeach
<div class="offcanvas-body flex-column pt-2 py-lg-0 filter-sidebar">
</div>
</div>
@endif
<div class="accordion">
@ -71,14 +51,8 @@
aria-labelledby="headingGenders">
<div class="accordion-body p-0 pb-4 mb-1 mb-xl-2">
<div style="height: 220px" data-simplebar data-simplebar-auto-hide="false">
<ul class="nav flex-column gap-2 pe-3">
@foreach ($genders as $gender)
<li class="nav-item mb-1">
<a class="nav-link d-block fw-normal p-0" href="{{ route('product.index', array_merge(request()->except('filter[gender]'), ['filter[gender]' => $gender->id])) }}">
{{ $gender->name }}
</a>
</li>
@endforeach
<ul class="nav flex-column gap-2 pe-3" id="genders-list">
<!-- Genders will be loaded here via AJAX -->
</ul>
</div>
</div>
@ -97,14 +71,8 @@
aria-labelledby="headingCategories">
<div class="accordion-body p-0 pb-4 mb-1 mb-xl-2">
<div style="height: 220px" data-simplebar data-simplebar-auto-hide="false">
<ul class="nav flex-column gap-2 pe-3">
@foreach ($categories as $category)
<li class="nav-item mb-1">
<a class="nav-link d-block fw-normal p-0" href="{{ route('product.index', array_merge(request()->except('filter[category]'), ['filter[category]' => $category->id])) }}">
{{ $category->name }}
</a>
</li>
@endforeach
<ul class="nav flex-column gap-2 pe-3" id="categories-list">
<!-- Categories will be loaded here via AJAX -->
</ul>
</div>
</div>
@ -471,7 +439,7 @@
<!-- 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">{{ count($products) }}</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">
@ -495,18 +463,9 @@
</div>
</div>
<div class="row gy-4 gy-md-5 pb-4 pb-md-5">
@foreach ($products as $key => $value)
<!-- Item -->
<div class="col-6 col-md-4 mb-2 mb-sm-3 mb-md-0">
<x-home.product-card :product="$value" />
</div>
@endforeach
<!-- Banner -->
<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="position-relative bg-body-tertiary text-center rounded-4 p-4 p-sm-5 py-md-4 py-xl-5">
@ -551,7 +510,7 @@
</div>
<!-- Show more button -->
<a href="{{ route('product.index', array_merge(request()->except('page'), ['page' => $page + 1])) }}" type="button" class="btn btn-lg btn-outline-secondary w-100">
<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>
@ -635,6 +594,264 @@
<i class="ci-filter fs-base me-2"></i>
Filters
</button>
<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 });
});
});
}
})
.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 = {}) {
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]);
}
});
// 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';
});
}
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>
<a type="button" href="#"
class="btn btn-sm btn-secondary bg-transparent border-0 text-decoration-underline p-0 ms-2 clear-all-filters">
Clear all
</a>
</div>
<div class="d-flex flex-wrap gap-2">
`;
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 += `
</div>
</div>
`;
// 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 {
// 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();
});
}
}
</script>
@endsection
@section('scripts')

View File

@ -24,4 +24,7 @@ Route::post('/locale/switch', [LocaleController::class, 'switch'])->name('locale
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/products',[ProductController::class, 'index'])->name('product.index');
Route::get('/products/ajax',[ProductController::class, 'ajax'])->name('product.ajax');
Route::get('/products/ajax/categories',[ProductController::class, 'categories'])->name('product.ajax.categories');
Route::get('/products/ajax/genders',[ProductController::class, 'genders'])->name('product.ajax.genders');
Route::get('/product/{slug}',[ProductController::class, 'detail'])->name('product.detail');