feat(filters): add filters for the main page

This commit is contained in:
2025-10-06 13:49:27 +03:00
parent bfc6ba496b
commit e7e045b695
65 changed files with 1172 additions and 525 deletions

108
spa/src/views/Filters.vue Normal file
View File

@@ -0,0 +1,108 @@
<template>
<div ref="goodsRef" class="pb-10">
<div class="flex flex-col">
<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="filtersStore.draft?.rules && Object.keys(filtersStore.draft.rules).length > 0"
v-for="(filter, filterId) in filtersStore.draft.rules"
>
<component
v-if="componentMap[filterId]"
:is="componentMap[filterId]"
:filter="filter"
/>
<p v-else>Not supported: {{ filterId }}</p>
</div>
<div v-else>
Нет фильтров
</div>
</main>
</div>
</div>
</template>
<script setup>
import {nextTick, onMounted, onUnmounted} from "vue";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import ProductPrice from "@/components/ProductFilters/Components/ProductPrice.vue";
import ForMainPage from "@/components/ProductFilters/Components/ForMainPage.vue";
import {useRouter} from "vue-router";
import ProductCategory from "@/components/ProductFilters/Components/ProductCategory/ProductCategory.vue";
defineOptions({
name: 'Filters'
});
const componentMap = {
RULE_PRODUCT_PRICE: ProductPrice,
RULE_PRODUCT_FOR_MAIN_PAGE: ForMainPage,
RULE_PRODUCT_CATEGORY: ProductCategory,
};
const router = useRouter();
const emit = defineEmits(['close', 'apply']);
const filtersStore = useProductFiltersStore();
const mainButton = window.Telegram.WebApp.MainButton;
const secondaryButton = window.Telegram.WebApp.SecondaryButton;
const haptic = window.Telegram.WebApp.HapticFeedback;
const applyFilters = async () => {
filtersStore.applied = JSON.parse(JSON.stringify(filtersStore.draft));
console.debug('Filters: apply filters. Hash for router: ', filtersStore.paramsHashForRouter);
haptic.impactOccurred('soft');
await nextTick();
router.back();
}
const resetFilters = async () => {
filtersStore.applied = await filtersStore.fetchFiltersForMainPage();
console.debug('Filters: reset filters. Hash for router: ', filtersStore.paramsHashForRouter);
haptic.notificationOccurred('success');
await nextTick();
window.scrollTo(0, 0);
router.back();
}
onMounted(async () => {
console.debug('Filters: OnMounted');
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);
if (filtersStore.applied?.rules) {
console.debug('Filters: Found applied filters.');
filtersStore.draft = JSON.parse(JSON.stringify(filtersStore.applied));
} else {
console.debug('No filters. Load filters from server');
filtersStore.draft = await filtersStore.fetchFiltersForMainPage();
}
});
onUnmounted(() => {
mainButton.hide();
secondaryButton.hide();
mainButton.offClick(applyFilters);
secondaryButton.offClick(resetFilters);
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div ref="goodsRef">
<div ref="goodsRef" class="pb-10">
<CategoriesInline/>
<div class="px-5 fixed z-50 w-full opacity-90" style="bottom: var(--tg-safe-area-inset-bottom);">
@@ -11,13 +11,12 @@
</div>
</div>
<ProductsList/>
<Filters
v-if="isFiltersShow"
:filters="productsStore.filters"
@apply="applyFilters"
@reset="resetFilters"
@close="closeFilters"
<ProductsList
:products="products"
:hasMore="hasMore"
:isLoading="isLoading"
:isLoadingMore="isLoadingMore"
@loadMore="onLoadMore"
/>
</div>
</template>
@@ -26,59 +25,78 @@
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 {onActivated, onMounted, ref, toRaw} from "vue";
import IconFunnel from "@/components/Icons/IconFunnel.vue";
import {FILTERS_MAIN_PAGE_DEFAULT} from "@/components/ProductFilters/filters.js";
import {useRoute} from "vue-router";
import {useRouter} from "vue-router";
import ftch from "@/utils/ftch.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
const route = useRoute();
const productsStore = useProductsStore();
defineOptions({
name: 'Home'
});
const isFiltersShow = ref(false);
const backButton = window.Telegram.WebApp.BackButton;
const router = useRouter();
const filtersStore = useProductFiltersStore();
const haptic = window.Telegram.WebApp.HapticFeedback;
const products = ref([]);
const hasMore = ref(false);
const isLoading = ref(false);
const isLoadingMore = ref(false);
const page = ref(1);
function showFilters() {
window.Telegram.WebApp.HapticFeedback.impactOccurred('soft');
isFiltersShow.value = true;
backButton.show();
haptic.impactOccurred('soft');
router.push({name: 'filters'});
}
function closeFilters() {
window.Telegram.WebApp.HapticFeedback.impactOccurred('rigid');
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()
async function fetchProducts() {
try {
isLoading.value = true;
console.debug('Home: Load products for Main Page.');
console.debug('Home: Fetch products from server using filters: ', toRaw(filtersStore.applied));
const response = await ftch('products', null, toRaw({
page: page.value,
filters: filtersStore.applied,
}));
products.value = response.data;
hasMore.value = response.meta.hasMore;
console.debug('Home: Products for main page loaded.');
} catch (error) {
console.error(error);
} finally {
isLoading.value = false;
}
}
onUnmounted(() => document.removeEventListener('click', handleClickOutside));
async function onLoadMore() {
try {
console.debug('Home: onLoadMore');
if (isLoading.value === true || isLoadingMore.value === true || hasMore.value === false) return;
isLoadingMore.value = true;
page.value++;
console.debug('Home: Load more for page ', page.value, ' using filters: ', toRaw(filtersStore.applied));
const response = await ftch('products', null, toRaw({
page: page.value,
filters: filtersStore.applied,
}));
products.value.push(...response.data);
hasMore.value = response.meta.hasMore;
} catch (error) {
console.error(error);
} finally {
isLoadingMore.value = false;
}
}
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);
}
console.debug("Home: Home Mounted");
console.debug("Home: Scroll top");
await fetchProducts();
window.scrollTo(0, 0);
});
onActivated(async () => {
console.debug('Home: Activated Home');
});
</script>

View File

@@ -1,11 +1,17 @@
<template>
<div ref="goodsRef">
<div ref="goodsRef" class="pb-10">
<div class="px-5 fixed z-50 w-full opacity-90" style="bottom: var(--tg-safe-area-inset-bottom);">
<div class="bg-base-300 flex justify-between p-2 rounded-xl shadow-md">
<SearchInput/>
</div>
</div>
<ProductsList/>
<ProductsList
:products="productsStore.products.data"
:hasMore="productsStore.products.meta.hasMore"
:isLoading="productsStore.isLoading"
:isLoadingMore="productsStore.isLoadingMore"
@loadMore="productsStore.loadMore"
/>
</div>
</template>
@@ -17,12 +23,17 @@ import {useRoute} from "vue-router";
import {useProductsStore} from "@/stores/ProductsStore.js";
import IconFunnel from "@/components/Icons/IconFunnel.vue";
defineOptions({
name: 'Products'
});
const route = useRoute();
const productsStore = useProductsStore();
const categoryId = route.params.category_id ?? null;
onMounted(async () => {
console.debug("Category Products Mounted");
console.debug("Load products for category: ", categoryId);
if (productsStore.filtersFullUrl === route.fullPath) {

View File

@@ -91,6 +91,5 @@ onUnmounted(() => {
onMounted(() => {
document.addEventListener('click', handleClickOutside);
nextTick(() => searchInput.value.focus());
});
</script>