search
This commit is contained in:
parent
c71b7ff5c2
commit
c7eba16c5f
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Items;
|
||||
use App\Models\StoreCategory;
|
||||
use App\Models\Gender;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function search(Request $request)
|
||||
{
|
||||
$query = $request->get('q');
|
||||
|
||||
if (empty($query) || strlen($query) < 2) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'results' => [],
|
||||
'message' => 'Please enter at least 2 characters'
|
||||
]);
|
||||
}
|
||||
|
||||
// Search products
|
||||
$products = Items::where('is_publish', true)
|
||||
->where('deleted_at', null)
|
||||
->where(function($q) use ($query) {
|
||||
$q->where('name', 'ILIKE', "%{$query}%")
|
||||
->orWhere('number', 'ILIKE', "%{$query}%")
|
||||
->orWhere('description', 'ILIKE', "%{$query}%");
|
||||
})
|
||||
->select('id', 'name', 'number', 'slug')
|
||||
->limit(8)
|
||||
->get();
|
||||
|
||||
// Search categories
|
||||
$categories = StoreCategory::where('name', 'ILIKE', "%{$query}%")
|
||||
->select('id', 'name')
|
||||
->limit(3)
|
||||
->get();
|
||||
|
||||
// Search genders
|
||||
$genders = Gender::where('name', 'ILIKE', "%{$query}%")
|
||||
->select('id', 'name')
|
||||
->limit(3)
|
||||
->get();
|
||||
|
||||
// Format results
|
||||
$results = [];
|
||||
|
||||
if ($products->isNotEmpty()) {
|
||||
$results['products'] = $products->map(function($product) {
|
||||
$price = $product->display_price;
|
||||
return [
|
||||
'id' => $product->id,
|
||||
'name' => $product->name,
|
||||
'number' => $product->number,
|
||||
'slug' => $product->slug ?? $product->id,
|
||||
'type' => 'product',
|
||||
'route' => route('product.detail', $product->slug)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if ($categories->isNotEmpty()) {
|
||||
$results['categories'] = $categories->map(function($category) {
|
||||
return [
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'slug' => $category->slug,
|
||||
'type' => 'category',
|
||||
'route' => route('product.index',['filter[category]'=>$category->id])
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if ($genders->isNotEmpty()) {
|
||||
$results['genders'] = $genders->map(function($gender) {
|
||||
return [
|
||||
'id' => $gender->id,
|
||||
'name' => $gender->name,
|
||||
'slug' => $gender->slug,
|
||||
'type' => 'gender',
|
||||
'route' => route('product.index',['filter[gender]'=>$gender->id])
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'results' => $results,
|
||||
'query' => $query
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,21 +39,29 @@ class ProductRepository
|
|||
|
||||
$where_ids = [];
|
||||
if ($price_range_start && $price_range_end) {
|
||||
$params_filter = [];
|
||||
$params_filter['location_id'] = $location_id;
|
||||
$params_filter['price_range_start'] = $price_range_start;
|
||||
$params_filter['price_range_end'] = $price_range_end;
|
||||
$params_filter = [
|
||||
'location_id' => $location_id,
|
||||
'price_range_start' => $price_range_start,
|
||||
'price_range_end' => $price_range_end,
|
||||
];
|
||||
|
||||
$where_ids = collect($this->getProductList($params_filter))->pluck('id')->toArray();
|
||||
$where_ids = collect($this->getProductList($params_filter))
|
||||
->pluck('id')
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
if (! empty($sorting_ids) && count($sorting_ids) > 0) {
|
||||
$where_ids = array_values(
|
||||
array_intersect($sorting_ids, $where_ids)
|
||||
);
|
||||
// Jika sudah ada sorting_ids → ikuti urutannya
|
||||
if (!empty($sorting_ids)) {
|
||||
$sorting_ids = array_values(array_intersect($sorting_ids, $where_ids));
|
||||
} else {
|
||||
// kalau belum ada sorting, pakai hasil filter
|
||||
$sorting_ids = $where_ids;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$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,
|
||||
|
|
@ -73,20 +81,18 @@ class ProductRepository
|
|||
$query->orderBy('percent', 'desc');
|
||||
} else {
|
||||
|
||||
if (! empty($sorting_ids) && count($sorting_ids) > 0) {
|
||||
if (!empty($sorting_ids)) {
|
||||
|
||||
$caseStatements = [];
|
||||
foreach ($sorting_ids as $index => $id) {
|
||||
$caseStatements[] = "WHEN items.id = {$id} THEN " . ($index + 1);
|
||||
}
|
||||
$caseStatements[] = "ELSE 999";
|
||||
$cases = collect($sorting_ids)->map(function ($id, $index) {
|
||||
return "WHEN items.id = {$id} THEN {$index}";
|
||||
})->implode(' ');
|
||||
|
||||
$query->orderByRaw("
|
||||
CASE
|
||||
" . implode("\n ", $caseStatements) . "
|
||||
END ASC
|
||||
{$cases}
|
||||
ELSE 999999
|
||||
END
|
||||
");
|
||||
|
||||
} elseif ($sort == 'new') {
|
||||
$query->orderByRaw("case when category1 in ('CLUBS','CLUB','COMPONENT HEAD') and brand = 'PXG' then 1 else 2 end ASC");
|
||||
} else {
|
||||
|
|
@ -113,8 +119,8 @@ class ProductRepository
|
|||
->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);
|
||||
->when($sorting_ids, function ($query) use ($sorting_ids) {
|
||||
$query->whereIn('items.id', $sorting_ids);
|
||||
});
|
||||
|
||||
if ($category_id) {
|
||||
|
|
@ -159,6 +165,7 @@ class ProductRepository
|
|||
$order_by = 'ORDER BY items.created_at DESC NULLS LAST';
|
||||
}
|
||||
|
||||
|
||||
$location_id = $params['location_id'] ?? null;
|
||||
|
||||
// =========================
|
||||
|
|
@ -210,13 +217,13 @@ class ProductRepository
|
|||
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)
|
||||
($location_id IS NOT NULL AND (d.location_id = $location_id OR d.location_id IS NULL))
|
||||
OR ($location_id IS NULL AND d.location_id IS NULL)
|
||||
)
|
||||
ORDER BY
|
||||
di.item_reference_id,
|
||||
CASE
|
||||
WHEN d.location_id = 22 THEN 1
|
||||
WHEN d.location_id = $location_id THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
d.created_at DESC
|
||||
|
|
@ -237,13 +244,13 @@ class ProductRepository
|
|||
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)
|
||||
($location_id IS NOT NULL AND (d.location_id = $location_id OR d.location_id IS NULL))
|
||||
OR ($location_id IS NULL AND d.location_id IS NULL)
|
||||
)
|
||||
ORDER BY
|
||||
di.item_reference_id,
|
||||
CASE
|
||||
WHEN d.location_id = 22 THEN 1
|
||||
WHEN d.location_id = $location_id THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
d.created_at DESC
|
||||
|
|
|
|||
|
|
@ -130,25 +130,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- search box --}}
|
||||
<div class="offcanvas offcanvas-top" id="searchBox" data-bs-backdrop="static" tabindex="-1">
|
||||
<div class="offcanvas-header border-bottom p-0 py-lg-1">
|
||||
<form class="container d-flex align-items-center">
|
||||
<input type="search" class="form-control form-control-lg fs-lg border-0 rounded-0 py-3 ps-0"
|
||||
<input type="search" id="searchInput" class="form-control form-control-lg fs-lg border-0 rounded-0 py-3 ps-0"
|
||||
placeholder="Search the products" data-autofocus="offcanvas">
|
||||
<button type="reset" class="btn-close fs-lg" data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="offcanvas-body px-0">
|
||||
<div class="container text-center">
|
||||
<svg class="text-body-tertiary opacity-60 mb-4" xmlns="http://www.w3.org/2000/svg" width="60"
|
||||
viewBox="0 0 512 512" fill="currentColor">
|
||||
<path
|
||||
d="M340.115,361.412l-16.98-16.98c-34.237,29.36-78.733,47.098-127.371,47.098C87.647,391.529,0,303.883,0,195.765S87.647,0,195.765,0s195.765,87.647,195.765,195.765c0,48.638-17.738,93.134-47.097,127.371l16.98,16.98l11.94-11.94c5.881-5.881,15.415-5.881,21.296,0l112.941,112.941c5.881,5.881,5.881,15.416,0,21.296l-45.176,45.176c-5.881,5.881-15.415,5.881-21.296,0L328.176,394.648c-5.881-5.881-5.881-15.416,0-21.296L340.115,361.412z M195.765,361.412c91.484,0,165.647-74.163,165.647-165.647S287.249,30.118,195.765,30.118S30.118,104.28,30.118,195.765S104.28,361.412,195.765,361.412z M360.12,384l91.645,91.645l23.88-23.88L384,360.12L360.12,384z M233.034,233.033c5.881-5.881,15.415-5.881,21.296,0c5.881,5.881,5.881,15.416,0,21.296c-32.345,32.345-84.786,32.345-117.131,0c-5.881-5.881-5.881-15.415,0-21.296c5.881-5.881,15.416-5.881,21.296,0C179.079,253.616,212.45,253.616,233.034,233.033zM135.529,180.706c-12.475,0-22.588-10.113-22.588-22.588c0-12.475,10.113-22.588,22.588-22.588c12.475,0,22.588,10.113,22.588,22.588C158.118,170.593,148.005,180.706,135.529,180.706z M256,180.706c-12.475,0-22.588-10.113-22.588-22.588c0-12.475,10.113-22.588,22.588-22.588s22.588,10.113,22.588,22.588C278.588,170.593,268.475,180.706,256,180.706z" />
|
||||
</svg>
|
||||
<h6 class="mb-2">Your search results will appear here</h6>
|
||||
<p class="fs-sm mb-0">Start typing in the search field above to see instant search results.</p>
|
||||
</div>
|
||||
<div class="offcanvas-body px-0 show_result" id="searchResults">
|
||||
<!-- Search results will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -362,3 +355,174 @@
|
|||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
let searchTimeout;
|
||||
|
||||
if (searchInput && searchResults) {
|
||||
// Show default state initially
|
||||
showDefaultSearch();
|
||||
|
||||
// Handle search input with debouncing
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
const query = this.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
showDefaultSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
searchTimeout = setTimeout(() => {
|
||||
performSearch(query);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Clear search when input is cleared
|
||||
searchInput.addEventListener('reset', function() {
|
||||
showDefaultSearch();
|
||||
});
|
||||
}
|
||||
|
||||
function showDefaultSearch() {
|
||||
searchResults.innerHTML = `
|
||||
<div class="container text-center py-5">
|
||||
<svg class="text-body-tertiary opacity-60 mb-4" xmlns="http://www.w3.org/2000/svg" width="60"
|
||||
viewBox="0 0 512 512" fill="currentColor">
|
||||
<path d="M340.115,361.412l-16.98-16.98c-34.237,29.36-78.733,47.098-127.371,47.098C87.647,391.529,0,303.883,0,195.765S87.647,0,195.765,0s195.765,87.647,195.765,195.765c0,48.638-17.738,93.134-47.097,127.371l16.98,16.98l11.94-11.94c5.881-5.881,15.415-5.881,21.296,0l112.941,112.941c5.881,5.881,5.881,15.416,0,21.296l-45.176,45.176c-5.881,5.881-15.415,5.881-21.296,0L328.176,394.648c-5.881-5.881-5.881-15.416,0-21.296L340.115,361.412z M195.765,361.412c91.484,0,165.647-74.163,165.647-165.647S287.249,30.118,195.765,30.118S30.118,104.28,30.118,195.765S104.28,361.412,195.765,361.412z M360.12,384l91.645,91.645l23.88-23.88L384,360.12L360.12,384z M233.034,233.033c5.881-5.881,15.415-5.881,21.296,0c5.881,5.881,5.881,15.416,0,21.296c-32.345,32.345-84.786,32.345-117.131,0c-5.881-5.881-5.881-15.415,0-21.296c5.881-5.881,15.416-5.881,21.296,0C179.079,253.616,212.45,253.616,233.034,233.033zM135.529,180.706c-12.475,0-22.588-10.113-22.588-22.588c0-12.475,10.113-22.588,22.588-22.588c12.475,0,22.588,10.113,22.588,22.588C158.118,170.593,148.005,180.706,135.529,180.706z M256,180.706c-12.475,0-22.588-10.113-22.588-22.588c0-12.475,10.113-22.588,22.588-22.588s22.588,10.113,22.588,22.588C278.588,170.593,268.475,180.706,256,180.706z" />
|
||||
</svg>
|
||||
<h6 class="mb-2">Your search results will appear here</h6>
|
||||
<p class="fs-sm mb-0">Start typing in the search field above to see instant search results.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function performSearch(query) {
|
||||
// Show loading state
|
||||
searchResults.innerHTML = `
|
||||
<div class="container text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 mb-0">Searching...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`{{ route('search.ajax') }}?q=${encodeURIComponent(query)}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
displaySearchResults(data.results, data.query);
|
||||
} else {
|
||||
showError('Search failed. Please try again.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Search error:', error);
|
||||
showError('Search failed. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
function displaySearchResults(results, query) {
|
||||
let html = '<div class="container py-4">';
|
||||
|
||||
// Products section
|
||||
if (results.products && results.products.length > 0) {
|
||||
html += `
|
||||
<div class="mb-4">
|
||||
<h6 class="text-uppercase fs-xs fw-bold mb-3">Products</h6>
|
||||
<div class="row g-3">
|
||||
`;
|
||||
|
||||
results.products.forEach(product => {
|
||||
html += `
|
||||
<div class="col-6 col-md-3">
|
||||
<a href="${product.route}" class="text-decoration-none">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-1 text-truncate">${product.name}</h6>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// Categories section
|
||||
if (results.categories && results.categories.length > 0) {
|
||||
html += `
|
||||
<div class="mb-4">
|
||||
<h6 class="text-uppercase fs-xs fw-bold mb-3">Categories</h6>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
`;
|
||||
|
||||
results.categories.forEach(category => {
|
||||
html += `
|
||||
<a href="${category.route}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
${category.name}
|
||||
</a>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// Genders section
|
||||
if (results.genders && results.genders.length > 0) {
|
||||
html += `
|
||||
<div class="mb-4">
|
||||
<h6 class="text-uppercase fs-xs fw-bold mb-3">Genders</h6>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
`;
|
||||
|
||||
results.genders.forEach(gender => {
|
||||
html += `
|
||||
<a href="${gender.route}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
${gender.name}
|
||||
</a>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// No results
|
||||
if (Object.keys(results).length === 0) {
|
||||
html += `
|
||||
<div class="text-center py-5">
|
||||
<p class="text-muted mb-0">No results found for "${query}"</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
searchResults.innerHTML = html;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
searchResults.innerHTML = `
|
||||
<div class="container text-center py-5">
|
||||
<div class="text-danger mb-3">
|
||||
<i class="ci-alert-circle fs-1"></i>
|
||||
</div>
|
||||
<p class="text-muted mb-0">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.landing', ['title' => 'Fashion Store - Catalog'])
|
||||
@extends('layouts.landing', ['title' => 'Store - Catalog'])
|
||||
|
||||
@section('content')
|
||||
<x-layout.header />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Http\Controllers\RoutingController;
|
|||
use App\Http\Controllers\LocationController;
|
||||
use App\Http\Controllers\LocaleController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
use App\Http\Controllers\SearchController;
|
||||
|
||||
Route::group(['prefix' => '/dummy'], function () {
|
||||
Route::get('', [RoutingController::class, 'index'])->name('root');
|
||||
|
|
@ -28,3 +29,6 @@ Route::get('/products/ajax',[ProductController::class, 'ajax'])->name('product.a
|
|||
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');
|
||||
|
||||
// Search routes
|
||||
Route::get('/search', [SearchController::class, 'search'])->name('search.ajax');
|
||||
Loading…
Reference in New Issue