feat: add filters to mainpage
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, orientation=portrait">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>OpenCart Telegram Mini App</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
978
spa/package-lock.json
generated
978
spa/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"js-md5": "^0.8.3",
|
||||
"ofetch": "^1.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"swiper": "^11.2.10",
|
||||
@@ -27,6 +28,6 @@
|
||||
"daisyui": "^5.0.46",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^7.0.3"
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="app-container h-full">
|
||||
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition name="route" mode="out-in" appear>
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
<Transition name="route" appear>
|
||||
<component :is="Component" :key="route.fullPath"/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<CartButton v-if="settings.store_enabled"/>
|
||||
@@ -11,8 +11,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {useWebAppViewport, useBackButton} from 'vue-tg';
|
||||
import {ref, watch} from "vue";
|
||||
import {useWebAppViewport} from 'vue-tg';
|
||||
import {useMiniApp, FullscreenViewport} from 'vue-tg';
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import CartButton from "@/components/CartButton.vue";
|
||||
@@ -28,47 +28,25 @@ disableVerticalSwipes();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const settings = useSettingsStore();
|
||||
const backButton = window.Telegram.WebApp.BackButton;
|
||||
const haptic = window.Telegram.WebApp.HapticFeedback;
|
||||
|
||||
function navigateBack() {
|
||||
haptic.impactOccurred('light');
|
||||
router.back();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
if (route.name === 'home') {
|
||||
window.Telegram.WebApp.BackButton.hide();
|
||||
window.Telegram.WebApp.BackButton.offClick();
|
||||
backButton.hide();
|
||||
backButton.offClick(navigateBack);
|
||||
} else {
|
||||
window.Telegram.WebApp.BackButton.show();
|
||||
window.Telegram.WebApp.BackButton.onClick(() => {
|
||||
window.Telegram.WebApp.HapticFeedback.impactOccurred('light');
|
||||
router.back();
|
||||
});
|
||||
backButton.show();
|
||||
backButton.onClick(navigateBack);
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
/* очень мягкий fade+slide */
|
||||
.route-enter-from,
|
||||
.route-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
.route-enter-active,
|
||||
.route-leave-active {
|
||||
transition: opacity .18s ease, transform .18s ease;
|
||||
}
|
||||
|
||||
/* уважение к reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.route-enter-active,
|
||||
.route-leave-active {
|
||||
transition: opacity .12s linear;
|
||||
}
|
||||
.route-enter-from,
|
||||
.route-leave-to {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
5
spa/src/components/BottomPanel.vue
Normal file
5
spa/src/components/BottomPanel.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col gap-2 border-t-1 border-t-base-300">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
7
spa/src/components/Icons/IconFunnel.vue
Normal file
7
spa/src/components/Icons/IconFunnel.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
21
spa/src/components/ProductFilters/Components/ForMainPage.vue
Normal file
21
spa/src/components/ProductFilters/Components/ForMainPage.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="border-b mb-5 border-b-base-200 flex justify-between">
|
||||
<div class="mb-2 text-base-content/40">Товары для главной страницы</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
disabled
|
||||
:checked="filter.criteria.product_for_main_page.params.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
filter: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="border-b mb-5 border-b-base-200">
|
||||
<div class="mb-2">Цена</div>
|
||||
<div class="flex justify-between">
|
||||
<label class="input mr-3">
|
||||
От
|
||||
<input
|
||||
type="number"
|
||||
class="grow"
|
||||
min="0"
|
||||
step="50"
|
||||
:placeholder="filter.criteria.product_price.params.value.from || '0'"
|
||||
v-model="filter.criteria.product_price.params.value.from"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="input">
|
||||
До
|
||||
<input
|
||||
type="number"
|
||||
class="grow"
|
||||
min="0"
|
||||
step="50"
|
||||
:placeholder="filter.criteria.product_price.params.value.to || '∞'"
|
||||
:value="filter.criteria.product_price.params.value.to"
|
||||
v-model="filter.criteria.product_price.params.value.to"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
filter: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
105
spa/src/components/ProductFilters/Filters.vue
Normal file
105
spa/src/components/ProductFilters/Filters.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
class="fixed top-0 left-0 z-50 w-full h-full bg-base-200 flex flex-col safe-top"
|
||||
>
|
||||
<header class="text-center shrink-0 p-3 font-bold text-xl">
|
||||
Фильтры
|
||||
</header>
|
||||
|
||||
<main class="mt-5 px-5 bg-base-200">
|
||||
<div
|
||||
v-if="draft?.rules && Object.keys(draft.rules).length > 0"
|
||||
v-for="(filter, filterId) in draft.rules"
|
||||
>
|
||||
<component
|
||||
v-if="componentMap[filterId]"
|
||||
:is="componentMap[filterId]"
|
||||
:filter="filter"
|
||||
/>
|
||||
|
||||
<p v-else>Not supported {{ filter.type }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-base-100 rounded-2xl p-5">
|
||||
Нет фильтров
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProductPrice from "@/components/ProductFilters/Components/ProductPrice.vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import ForMainPage from "@/components/ProductFilters/Components/ForMainPage.vue";
|
||||
|
||||
const componentMap = {
|
||||
RULE_PRODUCT_PRICE: ProductPrice,
|
||||
RULE_PRODUCT_FOR_MAIN_PAGE: ForMainPage,
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
closeOnOverlay: {type: Boolean, default: true},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'apply', 'reset']);
|
||||
|
||||
const mainButton = window.Telegram.WebApp.MainButton;
|
||||
const secondaryButton = window.Telegram.WebApp.SecondaryButton;
|
||||
const backButton = window.Telegram.WebApp.BackButton;
|
||||
const draft = ref({});
|
||||
|
||||
const applyFilters = () => {
|
||||
emit('apply', draft);
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function closeFiltersWithoutApply() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.document.body.style.overflow = 'hidden';
|
||||
|
||||
// Crete draft of the filters.
|
||||
draft.value = JSON.parse(JSON.stringify(props.filters));
|
||||
|
||||
mainButton.setParams({
|
||||
text: 'Применить',
|
||||
is_active: true,
|
||||
is_visible: true,
|
||||
});
|
||||
mainButton.show();
|
||||
mainButton.onClick(applyFilters);
|
||||
|
||||
secondaryButton.setParams({
|
||||
text: 'Сбросить фильтры',
|
||||
is_active: true,
|
||||
is_visible: true,
|
||||
position: 'top',
|
||||
});
|
||||
secondaryButton.show();
|
||||
secondaryButton.onClick(resetFilters);
|
||||
|
||||
backButton.show();
|
||||
backButton.onClick(closeFiltersWithoutApply);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mainButton.hide();
|
||||
secondaryButton.hide();
|
||||
mainButton.offClick(applyFilters);
|
||||
secondaryButton.offClick(resetFilters);
|
||||
backButton.hide();
|
||||
backButton.offClick(closeFiltersWithoutApply);
|
||||
window.document.body.style.overflow = '';
|
||||
});
|
||||
</script>
|
||||
31
spa/src/components/ProductFilters/filters.js
Normal file
31
spa/src/components/ProductFilters/filters.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export const FILTERS_MAIN_PAGE_DEFAULT = {
|
||||
operand: "AND",
|
||||
rules: {
|
||||
RULE_PRODUCT_PRICE: {
|
||||
criteria: {
|
||||
"product_price":
|
||||
{
|
||||
"type": "number",
|
||||
"params": {
|
||||
"operator": "between",
|
||||
"value": {
|
||||
"from": "",
|
||||
"to": ""
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RULE_PRODUCT_FOR_MAIN_PAGE: {
|
||||
"criteria": {
|
||||
"product_for_main_page": {
|
||||
"type": "boolean",
|
||||
"params": {
|
||||
"operator": "equals",
|
||||
"value": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="productsStore.hasMore === false"
|
||||
v-if="productsStore.products.meta.hasMore === false"
|
||||
class="text-xs text-center mt-4 pt-4 mb-2 border-t"
|
||||
>
|
||||
{{ settings.noMoreProductsMessage }}
|
||||
@@ -56,89 +56,24 @@ import NoProducts from "@/components/NoProducts.vue";
|
||||
import ProductImageSwiper from "@/components/ProductImageSwiper.vue";
|
||||
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||
import {useInfiniteScroll} from '@vueuse/core';
|
||||
import {useRoute} from "vue-router";
|
||||
import {useSettingsStore} from "@/stores/SettingsStore.js";
|
||||
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import {ref} from "vue";
|
||||
|
||||
const route = useRoute();
|
||||
const categoryId = route.params.category_id ?? null;
|
||||
const productsStore = useProductsStore();
|
||||
const settings = useSettingsStore();
|
||||
const bottom = ref(null);
|
||||
|
||||
function haptic() {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
productsStore.savedScrollY = window.scrollY;
|
||||
console.log("Store scrollY: ", productsStore.savedScrollY);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (productsStore.isLoading || productsStore.hasMore === false) return;
|
||||
|
||||
console.debug("Loading more...");
|
||||
console.debug("Page: ", productsStore.page);
|
||||
|
||||
productsStore.isLoading = true;
|
||||
try {
|
||||
const response = await productsStore.fetchProducts(categoryId, productsStore.page, true);
|
||||
productsStore.hasMore = response.meta.hasMore ?? false;
|
||||
productsStore.products.data.push(...response.data);
|
||||
productsStore.products.meta.currentCategoryName = response.meta.currentCategoryName;
|
||||
productsStore.page++;
|
||||
|
||||
console.log("Loaded products: ", productsStore.products.data.length);
|
||||
console.log("Has More? ", productsStore.hasMore);
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки', e)
|
||||
} finally {
|
||||
productsStore.isLoading = false;
|
||||
productsStore.loadFinished = true;
|
||||
}
|
||||
// productsStore.savedScrollY = window.scrollY;
|
||||
// console.log("Store scrollY: ", productsStore.savedScrollY);
|
||||
}
|
||||
|
||||
useInfiniteScroll(
|
||||
bottom,
|
||||
loadMore,
|
||||
async () => await productsStore.loadMore(),
|
||||
{distance: 1000}
|
||||
)
|
||||
|
||||
watch(() => route.params.id, async newId => {
|
||||
if (newId !== productsStore.savedCategoryId) {
|
||||
productsStore.reset()
|
||||
productsStore.savedCategoryId = newId
|
||||
await loadMore()
|
||||
}
|
||||
});
|
||||
|
||||
function handleClickOutside(e) {
|
||||
if (!e.target.closest('input, textarea')) {
|
||||
document.activeElement?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
const saved = productsStore.savedCategoryId === categoryId;
|
||||
if (saved && productsStore.products.data.length > 0) {
|
||||
await nextTick();
|
||||
// повторяем до тех пор, пока высота не станет больше savedScrollY
|
||||
const interval = setInterval(() => {
|
||||
const maxScroll = document.documentElement.scrollHeight - window.innerHeight
|
||||
if (maxScroll >= productsStore.savedScrollY) {
|
||||
window.scrollTo(0, productsStore.savedScrollY)
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 50);
|
||||
} else {
|
||||
productsStore.reset();
|
||||
productsStore.savedCategoryId = categoryId;
|
||||
await loadMore();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -9,9 +9,17 @@ import OrderCreated from "@/views/OrderCreated.vue";
|
||||
import Search from "@/views/Search.vue";
|
||||
|
||||
const routes = [
|
||||
{path: '/', name: 'home', component: Home},
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
},
|
||||
{path: '/product/:id', name: 'product.show', component: Product},
|
||||
{path: '/products/:category_id', name: 'product.categories.show', component: Products},
|
||||
{
|
||||
path: '/products/:category_id',
|
||||
name: 'product.categories.show',
|
||||
component: Products,
|
||||
},
|
||||
{path: '/categories', name: 'categories', component: CategoriesList},
|
||||
{path: '/category/:id', name: 'category.show', component: CategoriesList},
|
||||
{path: '/cart', name: 'cart', component: Cart},
|
||||
|
||||
23
spa/src/stores/ProductFiltersStore.js
Normal file
23
spa/src/stores/ProductFiltersStore.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import {defineStore} from "pinia";
|
||||
|
||||
|
||||
export const useProductFiltersStore = defineStore('product_filters', {
|
||||
state: () => ({
|
||||
filters: {},
|
||||
fullPath: '',
|
||||
}),
|
||||
|
||||
getters: {
|
||||
hasFilters: (state) => state.filters?.rules && Object.keys(state.filters.rules).length > 0,
|
||||
},
|
||||
|
||||
actions: {
|
||||
reset() {
|
||||
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.filters = {};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,47 +1,118 @@
|
||||
import {defineStore} from "pinia";
|
||||
import ftch from "../utils/ftch.js";
|
||||
import ftch from "@/utils/ftch.js";
|
||||
import {md5} from 'js-md5';
|
||||
import {toRaw} from "vue";
|
||||
|
||||
export const useProductsStore = defineStore('products', {
|
||||
state: () => ({
|
||||
products: {
|
||||
data: [],
|
||||
meta: {},
|
||||
meta: {
|
||||
hasMore: true,
|
||||
},
|
||||
},
|
||||
filters: null,
|
||||
filtersFullUrl: '',
|
||||
search: '',
|
||||
page: 1,
|
||||
isLoading: false,
|
||||
loadFinished: false,
|
||||
hasMore: true,
|
||||
savedCategoryId: null,
|
||||
savedScrollY: 0,
|
||||
currentLoadedParamsHash: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
paramsHash: (state) => md5(JSON.stringify(toRaw(state.getParams()))),
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchProducts(categoryId = null, page = 1, forMainPage = false) {
|
||||
getParams() {
|
||||
return {
|
||||
page: this.page,
|
||||
search: this.search,
|
||||
filters: toRaw(this.filters),
|
||||
};
|
||||
},
|
||||
|
||||
async fetchProducts() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
return await ftch('products', {
|
||||
categoryId: categoryId,
|
||||
page: page,
|
||||
search: this.search,
|
||||
forMainPage: forMainPage,
|
||||
});
|
||||
|
||||
console.debug('Current params hash: ', this.currentLoadedParamsHash);
|
||||
if (this.products.data.length > 0 && this.paramsHash === this.currentLoadedParamsHash) {
|
||||
console.debug('Loading products from cache');
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.products);
|
||||
});
|
||||
}
|
||||
|
||||
console.debug('Requested param cache: ', this.paramsHash);
|
||||
console.debug('Invalidate cache. Fetch products from server.', this.getParams());
|
||||
const response = await ftch('products', null, this.getParams());
|
||||
this.currentLoadedParamsHash = this.paramsHash;
|
||||
console.debug('Products loaded from server.');
|
||||
console.debug('New params hash: ', this.currentLoadedParamsHash);
|
||||
|
||||
return {
|
||||
meta: response.meta,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load products");
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadProducts(filters = null) {
|
||||
if (this.isLoading) return;
|
||||
|
||||
try {
|
||||
console.debug('Load products with filters', filters);
|
||||
console.debug('Filters for URL: ', this.filtersFullUrl);
|
||||
this.isLoading = false;
|
||||
this.page = 1;
|
||||
this.loadFinished = false;
|
||||
this.search = '';
|
||||
this.filters = filters;
|
||||
this.products = await this.fetchProducts();
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки', e);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.loadFinished = true;
|
||||
}
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (this.isLoading || this.products.meta.hasMore === false) return;
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.page++;
|
||||
console.debug('Load more products for page: ', this.page);
|
||||
const response = await this.fetchProducts();
|
||||
this.products.meta = response.meta;
|
||||
this.products.data.push(...response.data);
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки', e);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.loadFinished = true;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.isLoading = false;
|
||||
this.page = 1;
|
||||
this.hasMore = true;
|
||||
this.loadFinished = false;
|
||||
this.search = '';
|
||||
this.products = {
|
||||
data: [],
|
||||
meta: {},
|
||||
meta: {
|
||||
hasMore: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
3
spa/src/translations.js
Normal file
3
spa/src/translations.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
RULE_PRODUCT_PRICE: 'Цена',
|
||||
};
|
||||
@@ -1,8 +1,22 @@
|
||||
<template>
|
||||
<div ref="goodsRef" class="safe-top">
|
||||
<CategoriesInline/>
|
||||
<SearchInput/>
|
||||
|
||||
<div class="flex justify-between px-5">
|
||||
<button @click="showFilters" class="btn">
|
||||
<IconFunnel/>
|
||||
</button>
|
||||
<SearchInput/>
|
||||
</div>
|
||||
|
||||
<ProductsList/>
|
||||
<Filters
|
||||
v-if="isFiltersShow"
|
||||
:filters="productsStore.filters"
|
||||
@apply="applyFilters"
|
||||
@reset="resetFilters"
|
||||
@close="closeFilters"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,4 +24,58 @@
|
||||
import ProductsList from "@/components/ProductsList.vue";
|
||||
import CategoriesInline from "../components/CategoriesInline.vue";
|
||||
import SearchInput from "@/components/SearchInput.vue";
|
||||
import Filters from "@/components/ProductFilters/Filters.vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||
import IconFunnel from "@/components/Icons/IconFunnel.vue";
|
||||
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
|
||||
import {FILTERS_MAIN_PAGE_DEFAULT} from "@/components/ProductFilters/filters.js";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const productsStore = useProductsStore();
|
||||
|
||||
const isFiltersShow = ref(false);
|
||||
const backButton = window.Telegram.WebApp.BackButton;
|
||||
|
||||
function showFilters() {
|
||||
isFiltersShow.value = true;
|
||||
backButton.show();
|
||||
}
|
||||
|
||||
function closeFilters() {
|
||||
isFiltersShow.value = false;
|
||||
}
|
||||
|
||||
async function applyFilters(newFilters) {
|
||||
closeFilters();
|
||||
console.log("Load products with new filters: ", newFilters);
|
||||
productsStore.page = 1;
|
||||
await productsStore.loadProducts(newFilters);
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
closeFilters();
|
||||
productsStore.reset();
|
||||
await productsStore.loadProducts(FILTERS_MAIN_PAGE_DEFAULT);
|
||||
}
|
||||
|
||||
function handleClickOutside(e) {
|
||||
if (!e.target.closest('input, textarea')) {
|
||||
document.activeElement?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => document.removeEventListener('click', handleClickOutside));
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
if (productsStore.filtersFullUrl !== route.fullPath) {
|
||||
productsStore.filtersFullUrl = route.fullPath;
|
||||
await productsStore.loadProducts(FILTERS_MAIN_PAGE_DEFAULT);
|
||||
} else {
|
||||
await productsStore.loadProducts(productsStore.filters ?? FILTERS_MAIN_PAGE_DEFAULT);
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,4 +8,58 @@
|
||||
<script setup>
|
||||
import ProductsList from "@/components/ProductsList.vue";
|
||||
import SearchInput from "@/components/SearchInput.vue";
|
||||
import {onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useProductsStore} from "@/stores/ProductsStore.js";
|
||||
|
||||
const route = useRoute();
|
||||
const productsStore = useProductsStore();
|
||||
|
||||
const categoryId = route.params.category_id ?? null;
|
||||
|
||||
onMounted(async () => {
|
||||
console.debug("Load products for category: ", categoryId);
|
||||
|
||||
if (productsStore.filtersFullUrl === route.fullPath) {
|
||||
await productsStore.loadProducts(productsStore.filters ?? {
|
||||
operand: "AND",
|
||||
rules: {
|
||||
RULE_PRODUCT_CATEGORIES: {
|
||||
criteria: {
|
||||
product_category_ids: {
|
||||
type: "product_categories",
|
||||
params: {
|
||||
operator: "contains",
|
||||
value: [
|
||||
categoryId
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
productsStore.reset();
|
||||
productsStore.filtersFullUrl = route.fullPath;
|
||||
await productsStore.loadProducts({
|
||||
operand: "AND",
|
||||
rules: {
|
||||
RULE_PRODUCT_CATEGORIES: {
|
||||
criteria: {
|
||||
product_category_ids: {
|
||||
type: "product_categories",
|
||||
params: {
|
||||
operator: "contains",
|
||||
value: [
|
||||
categoryId
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user