Merge pull request #5272 from devansh-webkul/layered-navigation-optimization

Layered navigation ajaxified and shop routes enhanced  #4834
This commit is contained in:
Devansh 2021-10-26 15:22:53 +05:30 committed by GitHub
commit 02a19aa6a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 507 additions and 375 deletions

View File

@ -2,41 +2,73 @@
namespace Webkul\Product\Repositories;
use Illuminate\Container\Container as App;
use Webkul\Attribute\Repositories\AttributeRepository;
use Webkul\Core\Eloquent\Repository;
class ProductFlatRepository extends Repository
{
public function model()
{
return 'Webkul\Product\Contracts\ProductFlat';
}
/**
* Maximum Price of Category Product
* Create a new repository instance.
*
* @param \Webkul\Category\Contracts\Category $category
* @return float
* @param \Webkul\Attribute\Repositories\AttributeRepository $attributeRepository
* @param \Illuminate\Container\Container $app
* @return void
*/
public function getCategoryProductMaximumPrice($category = null)
{
static $loadedCategoryMaxPrice = [];
public function __construct(
AttributeRepository $attributeRepository,
App $app
) {
$this->attributeRepository = $attributeRepository;
if (! $category) {
return $this->model->max('max_price');
}
if (array_key_exists($category->id, $loadedCategoryMaxPrice)) {
return $loadedCategoryMaxPrice[$category->id];
}
return $loadedCategoryMaxPrice[$category->id] = $this->model
->leftJoin('product_categories', 'product_flat.product_id', 'product_categories.product_id')
->where('product_categories.category_id', $category->id)
->max('max_price');
parent::__construct($app);
}
/**
* get Category Product Attribute
* Specify model.
*
* @return string
*/
public function model(): string
{
return \Webkul\Product\Contracts\ProductFlat::class;
}
/**
* Get category product model.
*
* @param int $categoryId
* @return \Illuminate\Support\Querybuilder
*/
public function categoryProductQuerybuilder($categoryId)
{
return $this->model
->leftJoin('product_categories', 'product_flat.product_id', 'product_categories.product_id')
->where('product_categories.category_id', $categoryId)
->where('product_flat.channel', core()->getCurrentChannelCode())
->where('product_flat.locale', app()->getLocale());
}
/**
* Update `product_flat` custom column.
*
* @param \Webkul\Attribute\Models\Attribute $attribute
* @param \Webkul\Product\Listeners\ProductFlat $listener
* @return object
*/
public function updateAttributeColumn(
\Webkul\Attribute\Models\Attribute $attribute,
\Webkul\Product\Listeners\ProductFlat $listener
) {
return $this->model
->leftJoin('product_attribute_values as v', function ($join) use ($attribute) {
$join->on('product_flat.id', '=', 'v.product_id')
->on('v.attribute_id', '=', \DB::raw($attribute->id));
})->update(['product_flat.' . $attribute->code => \DB::raw($listener->attributeTypeFields[$attribute->type] . '_value')]);
}
/**
* Get category product attribute.
*
* @param int $categoryId
* @return array
@ -49,20 +81,20 @@ class ProductFlatRepository extends Repository
$productIds = $qb->pluck('product_flat.product_id')->toArray();
$childProductIds = $this->model->distinct()
->whereIn('parent_id', $productFlatIds)
->pluck('product_id')->toArray();
->whereIn('parent_id', $productFlatIds)
->pluck('product_id')->toArray();
$productIds = array_merge($productIds, $childProductIds);
$attributeValues = $this->model
->distinct()
->leftJoin('product_attribute_values as pa', 'product_flat.product_id', 'pa.product_id')
->leftJoin('attributes as at', 'pa.attribute_id', 'at.id')
->leftJoin('product_super_attributes as ps', 'product_flat.product_id', 'ps.product_id')
->select('pa.integer_value', 'pa.text_value', 'pa.attribute_id', 'ps.attribute_id as attributeId')
->where('is_filterable', 1)
->WhereIn('pa.product_id', $productIds)
->get();
->distinct()
->leftJoin('product_attribute_values as pa', 'product_flat.product_id', 'pa.product_id')
->leftJoin('attributes as at', 'pa.attribute_id', 'at.id')
->leftJoin('product_super_attributes as ps', 'product_flat.product_id', 'ps.product_id')
->select('pa.integer_value', 'pa.text_value', 'pa.attribute_id', 'ps.attribute_id as attributeId')
->where('is_filterable', 1)
->WhereIn('pa.product_id', $productIds)
->get();
$attributeInfo['attributeOptions'] = $attributeInfo['attributes'] = [];
@ -92,24 +124,9 @@ class ProductFlatRepository extends Repository
}
/**
* get Category Product Model
* Filter attributes according to products.
*
* @param int $categoryId
* @return \Illuminate\Support\Querybuilder
*/
public function categoryProductQuerybuilder($categoryId) {
return $this->model
->leftJoin('product_categories', 'product_flat.product_id', 'product_categories.product_id')
->where('product_categories.category_id', $categoryId)
->where('product_flat.channel', core()->getCurrentChannelCode())
->where('product_flat.locale', app()->getLocale());
}
/**
* filter attributes according to products
*
* @param array $category
* @param \Webkul\Category\Contracts\Category $category
* @return \Illuminate\Support\Collection
*/
public function getProductsRelatedFilterableAttributes($category)
@ -129,32 +146,78 @@ class ProductFlatRepository extends Repository
$allFilterableAttributes = array_filter(array_unique(array_intersect($categoryFilterableAttributes, $productCategoryArrributes['attributes'])));
$attributes = app('Webkul\Attribute\Repositories\AttributeRepository')->getModel()::with(['options' => function($query) use ($productCategoryArrributes) {
$attributes = $this->attributeRepository->getModel()::with([
'options' => function ($query) use ($productCategoryArrributes) {
return $query->whereIn('id', $productCategoryArrributes['attributeOptions'])
->orderBy('sort_order');
->orderBy('sort_order');
}
])->whereIn('id', $allFilterableAttributes)->get();
return $loadedCategoryAttributes[$category->id] = $attributes;
} else {
return $loadedCategoryAttributes[$category->id] = $category->filterableAttributes;
}
}
/**
* update product_flat custom column
* Maximum price of category product.
*
* @param \Webkul\Attribute\Models\Attribute $attribute
* @param \Webkul\Product\Listeners\ProductFlat $listener
* @param \Webkul\Category\Contracts\Category $category
* @return float
*/
public function updateAttributeColumn(
\Webkul\Attribute\Models\Attribute $attribute ,
\Webkul\Product\Listeners\ProductFlat $listener ) {
return $this->model
->leftJoin('product_attribute_values as v', function($join) use ($attribute) {
$join->on('product_flat.id', '=', 'v.product_id')
->on('v.attribute_id', '=', \DB::raw($attribute->id));
})->update(['product_flat.'.$attribute->code => \DB::raw($listener->attributeTypeFields[$attribute->type] .'_value')]);
public function getCategoryProductMaximumPrice($category = null)
{
static $loadedCategoryMaxPrice = [];
if (! $category) {
return $this->model->max('max_price');
}
if (array_key_exists($category->id, $loadedCategoryMaxPrice)) {
return $loadedCategoryMaxPrice[$category->id];
}
return $loadedCategoryMaxPrice[$category->id] = $this->model
->leftJoin('product_categories', 'product_flat.product_id', 'product_categories.product_id')
->where('product_categories.category_id', $category->id)
->max('max_price');
}
}
/**
* Get filter attributes.
*
* @param \Webkul\Category\Contracts\Category $category
* @return array
*/
public function getFilterAttributes($category)
{
$filterAttributes = [];
if (isset($category)) {
$filterAttributes = $this->getProductsRelatedFilterableAttributes($category);
}
if (! count($filterAttributes) > 0) {
$filterAttributes = $this->attributeRepository->getFilterAttributes();
}
return $filterAttributes;
}
/**
* Handle category product max price.
*
* @param \Webkul\Category\Contracts\Category $category
* @return float
*/
public function handleCategoryProductMaximumPrice($category)
{
$maxPrice = 0;
if (isset($category)) {
$maxPrice = core()->convertPrice($this->getCategoryProductMaximumPrice($category));
}
return $maxPrice;
}
}

View File

@ -3,70 +3,93 @@
namespace Webkul\Shop\Http\Controllers;
use Illuminate\Support\Facades\Storage;
use Webkul\Product\Repositories\ProductRepository;
use Webkul\Category\Repositories\CategoryRepository;
use Webkul\Product\Repositories\ProductAttributeValueRepository;
use Webkul\Product\Repositories\ProductDownloadableSampleRepository;
use Webkul\Product\Repositories\ProductDownloadableLinkRepository;
use Webkul\Product\Repositories\ProductDownloadableSampleRepository;
use Webkul\Product\Repositories\ProductFlatRepository;
use Webkul\Product\Repositories\ProductRepository;
class ProductController extends Controller
{
/**
* ProductRepository object
* Product repository instance.
*
* @var \Webkul\Product\Repositories\ProductRepository
*/
protected $productRepository;
/**
* ProductAttributeValueRepository object
* Product flat repository instance.
*
* @var \Webkul\Product\Repositories\ProductFlatRepository
*/
protected $productFlatRepository;
/**
* Product attribute value repository instance.
*
* @var \Webkul\Product\Repositories\ProductAttributeValueRepository
*/
protected $productAttributeValueRepository;
/**
* ProductDownloadableSampleRepository object
* Product downloadable sample repository instance.
*
* @var \Webkul\Product\Repositories\ProductDownloadableSampleRepository
*/
protected $productDownloadableSampleRepository;
/**
* ProductDownloadableLinkRepository object
* Product downloadable link repository instance.
*
* @var \Webkul\Product\Repositories\ProductDownloadableLinkRepository
*/
protected $productDownloadableLinkRepository;
/**
* Category repository instance.
*
* @var \Webkul\Category\Repositories\CategoryRepository
*/
protected $categoryRepository;
/**
* Create a new controller instance.
*
* @param \Webkul\Product\Repositories\ProductRepository $productRepository
* @param \Webkul\Product\Repositories\productAttributeValueRepository $productAttributeValueRepository
* @param \Webkul\Product\Repositories\ProductFlatRepository $productFlatRepository
* @param \Webkul\Product\Repositories\ProductAttributeValueRepository $productAttributeValueRepository
* @param \Webkul\Product\Repositories\ProductDownloadableSampleRepository $productDownloadableSampleRepository
* @param \Webkul\Product\Repositories\ProductDownloadableLinkRepository $productDownloadableLinkRepository
* @param \Webkul\Category\Repositories\CategoryRepository $categoryRepository
* @return void
*/
public function __construct(
ProductRepository $productRepository,
ProductFlatRepository $productFlatRepository,
ProductAttributeValueRepository $productAttributeValueRepository,
ProductDownloadableSampleRepository $productDownloadableSampleRepository,
ProductDownloadableLinkRepository $productDownloadableLinkRepository
)
{
ProductDownloadableLinkRepository $productDownloadableLinkRepository,
CategoryRepository $categoryRepository
) {
$this->productRepository = $productRepository;
$this->productFlatRepository = $productFlatRepository;
$this->productAttributeValueRepository = $productAttributeValueRepository;
$this->productDownloadableSampleRepository = $productDownloadableSampleRepository;
$this->productDownloadableLinkRepository = $productDownloadableLinkRepository;
$this->categoryRepository = $categoryRepository;
parent::__construct();
}
/**
* Download image or file
* Download image or file.
*
* @param int $productId
* @param int $attributeId
@ -102,7 +125,7 @@ class ProductController extends Controller
? $privateDisk->download($productDownloadableLink->sample_file)
: abort(404);
} else {
$fileName = $name = substr($productDownloadableLink->sample_url, strrpos($productDownloadableLink->sample_url, '/') + 1);
$fileName = substr($productDownloadableLink->sample_url, strrpos($productDownloadableLink->sample_url, '/') + 1);
$tempImage = tempnam(sys_get_temp_dir(), $fileName);
@ -116,7 +139,7 @@ class ProductController extends Controller
if ($productDownloadableSample->type == 'file') {
return Storage::download($productDownloadableSample->file);
} else {
$fileName = $name = substr($productDownloadableSample->url, strrpos($productDownloadableSample->url, '/') + 1);
$fileName = substr($productDownloadableSample->url, strrpos($productDownloadableSample->url, '/') + 1);
$tempImage = tempnam(sys_get_temp_dir(), $fileName);
@ -125,8 +148,48 @@ class ProductController extends Controller
return response()->download($tempImage, $fileName);
}
}
} catch(\Exception $e) {
} catch (\Exception $e) {
abort(404);
}
}
/**
* Get filter attributes for product.
*
* @return \Illuminate\Http\Response
*/
public function getFilterAttributes($categoryId)
{
$category = $this->categoryRepository->find($categoryId);
if ($category) {
return response()->json([
'filter_attributes' => $this->productFlatRepository->getFilterAttributes($category)
]);
}
return response()->json([
'filter_attributes' => []
]);
}
/**
* Get category product maximum price.
*
* @return \Illuminate\Http\Response
*/
public function getCategoryProductMaximumPrice($categoryId)
{
$category = $this->categoryRepository->find($categoryId);
if ($category) {
return response()->json([
'max_price' => $this->productFlatRepository->handleCategoryProductMaximumPrice($category)
]);
}
return response()->json([
'max_price' => 0
]);
}
}

View File

@ -27,7 +27,7 @@ class ShopServiceProvider extends ServiceProvider
]);
/* loaders */
$this->loadRoutesFrom(__DIR__ . '/../Http/routes.php');
$this->loadRoutesFrom(__DIR__ . '/../Routes/web.php');
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'shop');
$this->loadViewsFrom(__DIR__ . '/../Resources/views', 'shop');
@ -45,7 +45,7 @@ class ShopServiceProvider extends ServiceProvider
Paginator::defaultSimpleView('shop::partials.pagination');
/* breadcrumbs */
require __DIR__ . '/../Http/breadcrumbs.php';
require __DIR__ . '/../Routes/breadcrumbs.php';
}
/**

View File

@ -1,49 +1,28 @@
@inject ('attributeRepository', 'Webkul\Attribute\Repositories\AttributeRepository')
@inject ('productFlatRepository', 'Webkul\Product\Repositories\ProductFlatRepository')
@inject ('productRepository', 'Webkul\Product\Repositories\ProductRepository')
<?php
$filterAttributes = $attributes = [];
$maxPrice = 0;
if (isset($category)) {
$filterAttributes = $productFlatRepository->getProductsRelatedFilterableAttributes($category);
$maxPrice = core()->convertPrice($productFlatRepository->getCategoryProductMaximumPrice($category));
}
if (! count($filterAttributes) > 0) {
$filterAttributes = $attributeRepository->getFilterAttributes();
}
?>
<div class="layered-filter-wrapper">
{!! view_render_event('bagisto.shop.products.list.layered-nagigation.before') !!}
<layered-navigation></layered-navigation>
<layered-navigation></layered-navigation>
{!! view_render_event('bagisto.shop.products.list.layered-nagigation.after') !!}
</div>
@push('scripts')
<script type="text/x-template" id="layered-navigation-template">
<div>
<div class="filter-title">
{{ __('shop::app.products.layered-nav-title') }}
</div>
<div class="filter-content">
<div class="filter-attributes">
<filter-attribute-item v-for='(attribute, index) in attributes' :attribute="attribute" :key="index" :index="index" @onFilterAdded="addFilters(attribute.code, $event)" :appliedFilterValues="appliedFilters[attribute.code]">
<filter-attribute-item
v-for='(attribute, index) in attributes'
:key="index"
:index="index"
:attribute="attribute"
:appliedFilterValues="appliedFilters[attribute.code]"
@onFilterAdded="addFilters(attribute.code, $event)">
</filter-attribute-item>
</div>
</div>
</div>
@ -51,8 +30,7 @@
<script type="text/x-template" id="filter-attribute-item-template">
<div class="filter-attributes-item" :class="[active ? 'active' : '']">
<div class="filter-attributes-title" @click="active = !active">
<div class="filter-attributes-title" @click="active = ! active">
@{{ attribute.name ? attribute.name : attribute.admin_name }}
<div class="pull-right">
@ -65,16 +43,15 @@
</div>
<div class="filter-attributes-content">
<ol class="items" v-if="attribute.type != 'price'">
<li class="item" v-for='(option, index) in attribute.options'>
<span class="checkbox">
<input type="checkbox" :id="option.id" v-bind:value="option.id" v-model="appliedFilters" @change="addFilter($event)"/>
<label class="checkbox-view" :for="option.id"></label>
@{{ option.label ? option.label : option.admin_name }}
</span>
</li>
</ol>
@ -86,39 +63,47 @@
:tooltip-style="sliderConfig.tooltipStyle"
:max="sliderConfig.max"
:lazy="true"
@change="priceRangeUpdated($event)"
></vue-slider>
@change="priceRangeUpdated($event)">
</vue-slider>
</div>
</div>
</div>
</script>
<script>
Vue.component('layered-navigation', {
template: '#layered-navigation-template',
data: function() {
return {
attributes: @json($filterAttributes),
appliedFilters: {}
appliedFilters: {},
attributes: [],
}
},
created: function () {
var urlParams = new URLSearchParams(window.location.search);
this.setFilterAttributes();
var this_this = this;
urlParams.forEach(function (value, index) {
this_this.appliedFilters[index] = value.split(',');
});
this.setAppliedFilters();
},
methods: {
setFilterAttributes: function () {
axios
.get('{{ route('admin.catalog.products.get-filter-attributes', $category->id) }}')
.then((response) => {
this.attributes = response.data.filter_attributes;
});
},
setAppliedFilters: function () {
let urlParams = new URLSearchParams(window.location.search);
urlParams.forEach((value, index) => {
this.appliedFilters[index] = value.split(',');
});
},
addFilters: function (attributeCode, filters) {
if (filters.length) {
this.appliedFilters[attributeCode] = filters;
@ -130,7 +115,7 @@
},
applyFilter: function () {
var params = [];
let params = [];
for(key in this.appliedFilters) {
if (key != 'page') {
@ -144,27 +129,17 @@
});
Vue.component('filter-attribute-item', {
template: '#filter-attribute-item-template',
props: ['index', 'attribute', 'appliedFilterValues'],
data: function() {
let maxPrice = @json($maxPrice);
maxPrice = maxPrice ? ((parseInt(maxPrice) !== 0 || maxPrice) ? parseInt(maxPrice) : 500) : 500;
return {
appliedFilters: [],
active: false,
sliderConfig: {
value: [
0,
0
],
max: maxPrice,
value: [0, 0],
max: 500,
processStyle: {
"backgroundColor": "#FF6472"
},
@ -177,8 +152,7 @@
},
created: function () {
if (!this.index)
this.active = true;
if (! this.index) this.active = true;
if (this.appliedFilterValues && this.appliedFilterValues.length) {
this.appliedFilters = this.appliedFilterValues;
@ -189,9 +163,21 @@
this.active = true;
}
this.setMaxPrice();
},
methods: {
setMaxPrice: function () {
axios
.get('{{ route('admin.catalog.products.get-category-product-maximum-price', $category->id) }}')
.then((response) => {
let maxPrice = response.data.max_price;
this.sliderConfig.max = maxPrice ? ((parseInt(maxPrice) !== 0 || maxPrice) ? parseInt(maxPrice) : 500) : 500;
});
},
addFilter: function (e) {
this.$emit('onFilterAdded', this.appliedFilters)
},
@ -212,8 +198,6 @@
this.$emit('onFilterAdded', this.appliedFilters)
}
}
});
</script>
@endpush

View File

@ -0,0 +1,75 @@
<?php
use Illuminate\Support\Facades\Route;
use Webkul\Core\Http\Controllers\CountryStateController;
use Webkul\Shop\Http\Controllers\CartController;
use Webkul\Shop\Http\Controllers\OnepageController;
Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function () {
/**
* Country-State selector.
*/
Route::get('get/countries', [CountryStateController::class, 'getCountries'])->defaults('_config', [
'view' => 'shop::test'
])->name('get.countries');
/**
* Get States when Country is passed.
*/
Route::get('get/states/{country}', [CountryStateController::class, 'getStates'])->defaults('_config', [
'view' => 'shop::test'
])->name('get.states');
/**
* Cart routes.
*/
Route::get('checkout/cart', [CartController::class, 'index'])->defaults('_config', [
'view' => 'shop::checkout.cart.index'
])->name('shop.checkout.cart.index');
Route::post('checkout/cart/add/{id}', [CartController::class, 'add'])->defaults('_config', [
'redirect' => 'shop.checkout.cart.index'
])->name('cart.add');
Route::get('checkout/cart/remove/{id}', [CartController::class, 'remove'])->name('cart.remove');
Route::post('/checkout/cart', [CartController::class, 'updateBeforeCheckout'])->defaults('_config', [
'redirect' => 'shop.checkout.cart.index'
])->name('shop.checkout.cart.update');
Route::get('/checkout/cart/remove/{id}', [CartController::class, 'remove'])->defaults('_config', [
'redirect' => 'shop.checkout.cart.index'
])->name('shop.checkout.cart.remove');
Route::post('move/wishlist/{id}', [CartController::class, 'moveToWishlist'])->name('shop.movetowishlist');
/**
* Coupon routes.
*/
Route::post('checkout/cart/coupon', [CartController::class, 'applyCoupon'])->name('shop.checkout.cart.coupon.apply');
Route::delete('checkout/cart/coupon', [CartController::class, 'removeCoupon'])->name('shop.checkout.coupon.remove.coupon');
/**
* Checkout routes.
*/
Route::get('/checkout/onepage', [OnepageController::class, 'index'])->defaults('_config', [
'view' => 'shop::checkout.onepage'
])->name('shop.checkout.onepage.index');
Route::get('/checkout/summary', [OnepageController::class, 'summary'])->name('shop.checkout.summary');
Route::post('/checkout/save-address', [OnepageController::class, 'saveAddress'])->name('shop.checkout.save-address');
Route::post('/checkout/save-shipping', [OnepageController::class, 'saveShipping'])->name('shop.checkout.save-shipping');
Route::post('/checkout/save-payment', [OnepageController::class, 'savePayment'])->name('shop.checkout.save-payment');
Route::post('/checkout/check-minimum-order', [OnepageController::class, 'checkMinimumOrder'])->name('shop.checkout.check-minimum-order');
Route::post('/checkout/save-order', [OnepageController::class, 'saveOrder'])->name('shop.checkout.save-order');
Route::get('/checkout/success', [OnepageController::class, 'success'])->defaults('_config', [
'view' => 'shop::checkout.success'
])->name('shop.checkout.success');
});

View File

@ -1,8 +1,6 @@
<?php
// Controllers
use Webkul\CMS\Http\Controllers\Shop\PagePresenterController;
use Webkul\Core\Http\Controllers\CountryStateController;
use Illuminate\Support\Facades\Route;
use Webkul\Customer\Http\Controllers\AccountController;
use Webkul\Customer\Http\Controllers\AddressController;
use Webkul\Customer\Http\Controllers\CustomerController;
@ -11,156 +9,10 @@ use Webkul\Customer\Http\Controllers\RegistrationController;
use Webkul\Customer\Http\Controllers\ResetPasswordController;
use Webkul\Customer\Http\Controllers\SessionController;
use Webkul\Customer\Http\Controllers\WishlistController;
use Webkul\Shop\Http\Controllers\CartController;
use Webkul\Shop\Http\Controllers\DownloadableProductController;
use Webkul\Shop\Http\Controllers\HomeController;
use Webkul\Shop\Http\Controllers\OnepageController;
use Webkul\Shop\Http\Controllers\OrderController;
use Webkul\Shop\Http\Controllers\ProductController;
use Webkul\Shop\Http\Controllers\ReviewController;
use Webkul\Shop\Http\Controllers\SearchController;
use Webkul\Shop\Http\Controllers\SubscriptionController;
Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function () {
/**
* Store front home.
*/
Route::get('/', [HomeController::class, 'index'])->defaults('_config', [
'view' => 'shop::home.index'
])->name('shop.home.index');
/**
* Store front search.
*/
Route::get('/search', [SearchController::class, 'index'])->defaults('_config', [
'view' => 'shop::search.search'
])->name('shop.search.index');
Route::post('/upload-search-image', [HomeController::class, 'upload'])->name('shop.image.search.upload');
/**
* Subscription.
*/
Route::get('/subscribe', [SubscriptionController::class, 'subscribe'])->name('shop.subscribe');
Route::get('/unsubscribe/{token}', [SubscriptionController::class, 'unsubscribe'])->name('shop.unsubscribe');
/**
* Country-State selector.
*/
Route::get('get/countries', [CountryStateController::class, 'getCountries'])->defaults('_config', [
'view' => 'shop::test'
])->name('get.countries');
/**
* Get States when Country is passed.
*/
Route::get('get/states/{country}', [CountryStateController::class, 'getStates'])->defaults('_config', [
'view' => 'shop::test'
])->name('get.states');
/**
* Cart, coupons and checkout.
*/
// Cart items listing.
Route::get('checkout/cart', [CartController::class, 'index'])->defaults('_config', [
'view' => 'shop::checkout.cart.index'
])->name('shop.checkout.cart.index');
// Add cart items.
Route::post('checkout/cart/add/{id}', [CartController::class, 'add'])->defaults('_config', [
'redirect' => 'shop.checkout.cart.index'
])->name('cart.add');
// Cart items remove.
Route::get('checkout/cart/remove/{id}', [CartController::class, 'remove'])->name('cart.remove');
// Cart update before checkout.
Route::post('/checkout/cart', [CartController::class, 'updateBeforeCheckout'])->defaults('_config', [
'redirect' => 'shop.checkout.cart.index'
])->name('shop.checkout.cart.update');
// Cart items remove.
Route::get('/checkout/cart/remove/{id}', [CartController::class, 'remove'])->defaults('_config', [
'redirect' => 'shop.checkout.cart.index'
])->name('shop.checkout.cart.remove');
// Move item to wishlist from cart.
Route::post('move/wishlist/{id}', [CartController::class, 'moveToWishlist'])->name('shop.movetowishlist');
// Apply coupons.
Route::post('checkout/cart/coupon', [CartController::class, 'applyCoupon'])->name('shop.checkout.cart.coupon.apply');
// Remove coupons.
Route::delete('checkout/cart/coupon', [CartController::class, 'removeCoupon'])->name('shop.checkout.coupon.remove.coupon');
// Checkout index page.
Route::get('/checkout/onepage', [OnepageController::class, 'index'])->defaults('_config', [
'view' => 'shop::checkout.onepage'
])->name('shop.checkout.onepage.index');
// Checkout get summary.
Route::get('/checkout/summary',[OnepageController::class, 'summary'])->name('shop.checkout.summary');
// Checkout save address form.
Route::post('/checkout/save-address', [OnepageController::class, 'saveAddress'])->name('shop.checkout.save-address');
// Checkout save shipping address form.
Route::post('/checkout/save-shipping', [OnepageController::class, 'saveShipping'])->name('shop.checkout.save-shipping');
// Checkout save payment method form.
Route::post('/checkout/save-payment', [OnepageController::class, 'savePayment'])->name('shop.checkout.save-payment');
// Check minimum order.
Route::post('/checkout/check-minimum-order', [OnepageController::class, 'checkMinimumOrder'])->name('shop.checkout.check-minimum-order');
// Checkout save order.
Route::post('/checkout/save-order', [OnepageController::class, 'saveOrder'])->name('shop.checkout.save-order');
// Checkout order successful.
Route::get('/checkout/success', [OnepageController::class, 'success'])->defaults('_config', [
'view' => 'shop::checkout.success'
])->name('shop.checkout.success');
/**
* Product reviews.
*/
// Show product review form.
Route::get('/reviews/{slug}', [ReviewController::class, 'show'])->defaults('_config', [
'view' => 'shop::products.reviews.index'
])->name('shop.reviews.index');
// Show product review listing.
Route::get('/product/{slug}/review', [ReviewController::class, 'create'])->defaults('_config', [
'view' => 'shop::products.reviews.create'
])->name('shop.reviews.create');
// Store product review.
Route::post('/product/{slug}/review', [ReviewController::class, 'store'])->defaults('_config', [
'redirect' => 'shop.home.index'
])->name('shop.reviews.store');
/**
* Downloadable products.
*/
Route::get('/downloadable/download-sample/{type}/{id}', [ProductController::class, 'downloadSample'])->name('shop.downloadable.download_sample');
Route::get('/product/{id}/{attribute_id}', [ProductController::class, 'download'])->defaults('_config', [
'view' => 'shop.products.index'
])->name('shop.product.file.download');
/**
* These are the routes which are used during checkout for checking the customer.
* So, placed outside the cart merger middleware.
*/
Route::prefix('customer')->group(function () {
// For customer exist check.
Route::post('/customer/exist', [OnepageController::class, 'checkExistCustomer'])->name('customer.checkout.exist');
// For customer login checkout.
Route::post('/customer/checkout/login', [OnepageController::class, 'loginForCheckout'])->name('customer.checkout.login');
});
/**
* Cart merger middleware. This middleware will take care of the items
* which are deactivated at the time of buy now functionality. If somehow
@ -171,40 +23,31 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function
* group.
*/
Route::group(['middleware' => ['cart.merger']], function () {
/**
* Customer routes.
*/
Route::prefix('customer')->group(function () {
/**
* Forgot password routes.
*/
// Show forgot password form.
Route::get('/forgot-password', [ForgotPasswordController::class, 'create'])->defaults('_config', [
'view' => 'shop::customers.signup.forgot-password'
])->name('customer.forgot-password.create');
// Store forgot password.
Route::post('/forgot-password', [ForgotPasswordController::class, 'store'])->name('customer.forgot-password.store');
// Reset password form.
Route::get('/reset-password/{token}', [ResetPasswordController::class, 'create'])->defaults('_config', [
'view' => 'shop::customers.signup.reset-password'
])->name('customer.reset-password.create');
// Store reset password.
Route::post('/reset-password',[ResetPasswordController::class, 'store'])->defaults('_config', [
Route::post('/reset-password', [ResetPasswordController::class, 'store'])->defaults('_config', [
'redirect' => 'customer.profile.index'
])->name('customer.reset-password.store');
/**
* Login routes.
*/
// Login form.
Route::get('login', [SessionController::class, 'show'])->defaults('_config', [
'view' => 'shop::customers.session.index',
])->name('customer.session.index');
// Login.
Route::post('login', [SessionController::class, 'create'])->defaults('_config', [
'redirect' => 'customer.profile.index'
])->name('customer.session.create');
@ -212,24 +55,23 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function
/**
* Registration routes.
*/
// Show registration form.
Route::get('register', [RegistrationController::class, 'show'])->defaults('_config', [
'view' => 'shop::customers.signup.index'
])->name('customer.register.index');
// Store new registered user.
Route::post('register', [RegistrationController::class, 'create'])->defaults('_config', [
'redirect' => 'customer.session.index',
])->name('customer.register.create');
// Verify account.
/**
* Customer verification routes.
*/
Route::get('/verify-account/{token}', [RegistrationController::class, 'verifyAccount'])->name('customer.verify');
// Resend verification email.
Route::get('/resend/verification/{email}', [RegistrationController::class, 'resendVerificationEmail'])->name('customer.resend.verification-email');
/**
* Customer authented routes. All the below routes only be accessible
* Customer authenticated routes. All the below routes only be accessible
* if customer is authenticated.
*/
Route::group(['middleware' => ['customer']], function () {
@ -362,20 +204,21 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function
});
});
});
});
/**
* These are the routes which are used during checkout for checking the customer.
* So, placed outside the cart merger middleware.
*/
Route::prefix('customer')->group(function () {
/**
* For customer exist check.
*/
Route::post('/customer/exist', [OnepageController::class, 'checkExistCustomer'])->name('customer.checkout.exist');
/**
* CMS pages.
* For customer login checkout.
*/
Route::get('page/{slug}', [PagePresenterController::class, 'presenter'])->name('shop.cms.page');
/**
* Fallback route.
*/
Route::fallback(\Webkul\Shop\Http\Controllers\ProductsCategoriesProxyController::class . '@index')
->defaults('_config', [
'product_view' => 'shop::products.view',
'category_view' => 'shop::products.index'
])
->name('shop.productOrCategory.index');
Route::post('/customer/checkout/login', [OnepageController::class, 'loginForCheckout'])->name('customer.checkout.login');
});
});

View File

@ -0,0 +1,85 @@
<?php
use Illuminate\Support\Facades\Route;
use Webkul\CMS\Http\Controllers\Shop\PagePresenterController;
use Webkul\Shop\Http\Controllers\HomeController;
use Webkul\Shop\Http\Controllers\ProductController;
use Webkul\Shop\Http\Controllers\ReviewController;
use Webkul\Shop\Http\Controllers\SearchController;
use Webkul\Shop\Http\Controllers\SubscriptionController;
Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function () {
/**
* Cart merger middleware. This middleware will take care of the items
* which are deactivated at the time of buy now functionality. If somehow
* user redirects without completing the checkout then this will merge
* full cart.
*
* If some routes are not able to merge the cart, then place the route in this
* group.
*/
Route::group(['middleware' => ['cart.merger']], function () {
/**
* CMS pages.
*/
Route::get('page/{slug}', [PagePresenterController::class, 'presenter'])->name('shop.cms.page');
/**
* Fallback route.
*/
Route::fallback(\Webkul\Shop\Http\Controllers\ProductsCategoriesProxyController::class . '@index')
->defaults('_config', [
'product_view' => 'shop::products.view',
'category_view' => 'shop::products.index'
])
->name('shop.productOrCategory.index');
});
/**
* Store front home.
*/
Route::get('/', [HomeController::class, 'index'])->defaults('_config', [
'view' => 'shop::home.index'
])->name('shop.home.index');
/**
* Store front search.
*/
Route::get('/search', [SearchController::class, 'index'])->defaults('_config', [
'view' => 'shop::search.search'
])->name('shop.search.index');
Route::post('/upload-search-image', [HomeController::class, 'upload'])->name('shop.image.search.upload');
/**
* Subscription routes.
*/
Route::get('/subscribe', [SubscriptionController::class, 'subscribe'])->name('shop.subscribe');
Route::get('/unsubscribe/{token}', [SubscriptionController::class, 'unsubscribe'])->name('shop.unsubscribe');
/**
* Product and categories routes.
*/
Route::get('/reviews/{slug}', [ReviewController::class, 'show'])->defaults('_config', [
'view' => 'shop::products.reviews.index'
])->name('shop.reviews.index');
Route::get('/product/{slug}/review', [ReviewController::class, 'create'])->defaults('_config', [
'view' => 'shop::products.reviews.create'
])->name('shop.reviews.create');
Route::post('/product/{slug}/review', [ReviewController::class, 'store'])->defaults('_config', [
'redirect' => 'shop.home.index'
])->name('shop.reviews.store');
Route::get('/downloadable/download-sample/{type}/{id}', [ProductController::class, 'downloadSample'])->name('shop.downloadable.download_sample');
Route::get('/product/{id}/{attribute_id}', [ProductController::class, 'download'])->defaults('_config', [
'view' => 'shop.products.index'
])->name('shop.product.file.download');
Route::get('products/get-filter-attributes/{categoryId}', [ProductController::class, 'getFilterAttributes'])->name('admin.catalog.products.get-filter-attributes');
Route::get('products/get-category-product-maximum-price/{categoryId}', [ProductController::class, 'getCategoryProductMaximumPrice'])->name('admin.catalog.products.get-category-product-maximum-price');
});

View File

@ -0,0 +1,18 @@
<?php
/**
* Store front routes.
*/
require 'store-front-routes.php';
/**
* Customer routes. All routes related to customer
* in storefront will be placed here.
*/
require 'customer-routes.php';
/**
* Checkout routes. All routes related to checkout like
* cart, coupons, etc will be placed here.
*/
require 'checkout-routes.php';

View File

@ -1,30 +1,9 @@
@inject ('productRepository', 'Webkul\Product\Repositories\ProductRepository')
@inject ('attributeRepository', 'Webkul\Attribute\Repositories\AttributeRepository')
@inject ('productFlatRepository', 'Webkul\Product\Repositories\ProductFlatRepository')
<?php
$filterAttributes = $attributes = [];
$maxPrice = 0;
if (isset($category)) {
$filterAttributes = $productFlatRepository->getProductsRelatedFilterableAttributes($category);
$maxPrice = core()->convertPrice($productFlatRepository->getCategoryProductMaximumPrice($category));
}
if (! count($filterAttributes) > 0) {
$filterAttributes = $attributeRepository->getFilterAttributes();
}
?>
<div class="layered-filter-wrapper left">
{!! view_render_event('bagisto.shop.products.list.layered-nagigation.before') !!}
<layered-navigation></layered-navigation>
{!! view_render_event('bagisto.shop.products.list.layered-nagigation.after') !!}
</div>
@push('scripts')
@ -38,14 +17,13 @@
<div class="filter-content">
<div class="filter-attributes">
<filter-attribute-item
v-for='(attribute, index) in attributes'
:key="index"
:index="index"
:attribute="attribute"
v-for='(attribute, index) in attributes'
@onFilterAdded="addFilters(attribute.code, $event)"
:appliedFilterValues="appliedFilters[attribute.code]">
:appliedFilterValues="appliedFilters[attribute.code]"
@onFilterAdded="addFilters(attribute.code, $event)">
</filter-attribute-item>
</div>
</div>
</div>
@ -53,7 +31,7 @@
<script type="text/x-template" id="filter-attribute-item-template">
<div :class="`cursor-pointer filter-attributes-item ${active ? 'active' : ''}`">
<div class="filter-attributes-title" @click="active = !active">
<div class="filter-attributes-title" @click="active = ! active">
<h6 class="fw6 display-inbl">@{{ attribute.name ? attribute.name : attribute.admin_name }}</h6>
<div class="float-right display-table">
@ -70,7 +48,6 @@
<li
class="item"
v-for='(option, index) in attribute.options'>
<div
class="checkbox"
@click="optionClicked(option.id, $event)">
@ -99,18 +76,19 @@
<div class="filter-input row col-12 no-padding">
<input
type="text"
disabled
name="price_from"
:value="sliderConfig.priceFrom"
id="price_from" />
id="price_from"
disabled>
<label class="col text-center" for="to">{{ __('shop::app.products.filter-to') }}</label>
<input
type="text"
disabled
name="price_to"
:value="sliderConfig.priceTo"
id="price_to">
type="text"
name="price_to"
:value="sliderConfig.priceTo"
id="price_to"
disabled>
</div>
</div>
</div>
@ -120,22 +98,37 @@
<script>
Vue.component('layered-navigation', {
template: '#layered-navigation-template',
data: function() {
return {
appliedFilters: {},
attributes: @json($filterAttributes),
attributes: [],
}
},
created: function () {
let urlParams = new URLSearchParams(window.location.search);
this.setFilterAttributes();
urlParams.forEach((value, index) => {
this.appliedFilters[index] = value.split(',');
});
this.setAppliedFilters();
},
methods: {
setFilterAttributes: function () {
axios
.get('{{ route('admin.catalog.products.get-filter-attributes', $category->id) }}')
.then((response) => {
this.attributes = response.data.filter_attributes;
});
},
setAppliedFilters: function () {
let urlParams = new URLSearchParams(window.location.search);
urlParams.forEach((value, index) => {
this.appliedFilters[index] = value.split(',');
});
},
addFilters: function (attributeCode, filters) {
if (filters.length) {
this.appliedFilters[attributeCode] = filters;
@ -147,7 +140,7 @@
},
applyFilter: function () {
var params = [];
let params = [];
for (key in this.appliedFilters) {
if (key != 'page') {
@ -162,6 +155,7 @@
Vue.component('filter-attribute-item', {
template: '#filter-attribute-item-template',
props: [
'index',
'attribute',
@ -170,26 +164,19 @@
],
data: function() {
let maxPrice = @json($maxPrice);
maxPrice = maxPrice ? ((parseInt(maxPrice) !== 0 || maxPrice) ? parseInt(maxPrice) : 500) : 500;
return {
active: false,
appliedFilters: [],
sliderConfig: {
max: maxPrice,
value: [ 0, 0 ],
max: 500,
value: [0, 0],
processStyle: {
"backgroundColor": "#FF6472"
},
tooltipStyle: {
"borderColor": "#FF6472",
"backgroundColor": "#FF6472",
},
priceTo: 0,
priceFrom: 0,
}
@ -197,11 +184,11 @@
},
created: function () {
if (!this.index)
this.active = false;
if (! this.index) this.active = false;
if (this.appliedFilterValues && this.appliedFilterValues.length) {
this.appliedFilters = this.appliedFilterValues;
if (this.attribute.type == 'price') {
this.sliderConfig.value = this.appliedFilterValues;
this.sliderConfig.priceFrom = this.appliedFilterValues[0];
@ -210,9 +197,21 @@
this.active = true;
}
this.setMaxPrice();
},
methods: {
setMaxPrice: function () {
axios
.get('{{ route('admin.catalog.products.get-category-product-maximum-price', $category->id) }}')
.then((response) => {
let maxPrice = response.data.max_price;
this.sliderConfig.max = maxPrice ? ((parseInt(maxPrice) !== 0 || maxPrice) ? parseInt(maxPrice) : 500) : 500;
});
},
addFilter: function (e) {
this.$emit('onFilterAdded', this.appliedFilters)
},
@ -234,14 +233,16 @@
optionClicked: function (id, {target}) {
let checkbox = $(`#${id}`);
if (checkbox && checkbox.length > 0 && target.type != "checkbox") {
checkbox = checkbox[0];
checkbox.checked = !checkbox.checked;
checkbox.checked = ! checkbox.checked;
if (checkbox.checked) {
this.appliedFilters.push(id);
} else {
let idPosition = this.appliedFilters.indexOf(id);
if (idPosition == -1)
idPosition = this.appliedFilters.indexOf(id.toString());