From 64ead29583086dc55ae59e5d2b775dae31f36944 Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Mon, 1 Dec 2025 21:55:16 +0300 Subject: [PATCH] feat(search): improve search UI with sticky bar and keyboard handling - Add fixed search bar with glassmorphism effect (backdrop blur, semi-transparent) - Implement clear search button in DaisyUI style - Auto-hide keyboard on Enter key press and scroll events - Remove search page title for cleaner UI - Use DaisyUI theme-aware background colors instead of fixed white - Add fixed padding offset for content below search bar --- frontend/spa/src/components/ProductsList.vue | 2 - frontend/spa/src/helpers.js | 28 +++ frontend/spa/src/stores/SearchStore.js | 48 ++++- frontend/spa/src/views/Search.vue | 181 +++++++++++++----- .../src/Handlers/ProductsHandler.php | 2 +- .../src/Services/ProductsService.php | 2 + 6 files changed, 210 insertions(+), 53 deletions(-) diff --git a/frontend/spa/src/components/ProductsList.vue b/frontend/spa/src/components/ProductsList.vue index 3552947..22302cd 100644 --- a/frontend/spa/src/components/ProductsList.vue +++ b/frontend/spa/src/components/ProductsList.vue @@ -68,8 +68,6 @@ import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; import IconFunnel from "@/components/Icons/IconFunnel.vue"; import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useRouter} from "vue-router"; -import ProductTitle from "@/components/ProductItem/ProductTitle.vue"; -import Price from "@/components/ProductItem/Price.vue"; import PriceTitle from "@/components/ProductItem/PriceTitle.vue"; const router = useRouter(); diff --git a/frontend/spa/src/helpers.js b/frontend/spa/src/helpers.js index 5ad1a87..efb932e 100644 --- a/frontend/spa/src/helpers.js +++ b/frontend/spa/src/helpers.js @@ -123,3 +123,31 @@ export function deserializeStartParams(serialized) { return parameters; } + +export function renderSmartNumber(count) { + if (count < 10) { + return count.toString(); + } + + if (count < 100) { + // округляем до десятков + const tens = Math.floor(count / 10) * 10; + return `+${tens}`; + } + + if (count < 1000) { + // округляем до сотен + const hundreds = Math.floor(count / 100) * 100; + return `+${hundreds}`; + } + + // для тысяч и выше + if (count < 1_000_000) { + const rounded = Math.floor(count / 1_000) * 1; + return `+${rounded}к`; + } + + // миллионы + const roundedMillions = Math.floor(count / 1_000_000); + return `+${roundedMillions}м`; +} \ No newline at end of file diff --git a/frontend/spa/src/stores/SearchStore.js b/frontend/spa/src/stores/SearchStore.js index 68aa0c3..38a0df8 100644 --- a/frontend/spa/src/stores/SearchStore.js +++ b/frontend/spa/src/stores/SearchStore.js @@ -2,6 +2,7 @@ import {defineStore} from "pinia"; import ftch from "@/utils/ftch.js"; import {YA_METRIKA_GOAL} from "@/constants/yaMetrikaGoals.js"; import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; +import {toRaw} from "vue"; export const useSearchStore = defineStore('search', { state: () => ({ @@ -13,7 +14,9 @@ export const useSearchStore = defineStore('search', { }, isLoading: false, + isLoadingMore: false, isSearchPerformed: false, + hasMore: false, }), actions: { @@ -28,6 +31,14 @@ export const useSearchStore = defineStore('search', { }; }, + async fetchProducts(search, page = 1, perPage = 5) { + return await ftch('products', { + page, + perPage: perPage, + search, + }); + }, + async performSearch() { if (!this.search) { return this.reset(); @@ -39,11 +50,10 @@ export const useSearchStore = defineStore('search', { try { this.isLoading = true; - this.products = await ftch('products', { - page: this.page, - perPage: 10, - search: this.search, - }); + const response = await this.fetchProducts(this.search, this.page, 10); + console.debug('[Search] Perform Search: ', response); + this.products = response; + this.hasMore = response?.meta?.hasMore || false; } catch (error) { console.error(error); } finally { @@ -51,6 +61,34 @@ export const useSearchStore = defineStore('search', { this.isSearchPerformed = true; } }, + + async loadMore() { + try { + if (this.isLoading === true || this.isLoadingMore === true || this.hasMore === false) return; + this.isLoadingMore = true; + this.page++; + + console.debug('[Search] Loading more products for page: ', this.page); + + const response = await ftch('products', null, toRaw({ + page: this.page, + perPage: 10, + search: this.search, + })); + + console.debug('[Search] Search results: ', response); + + this.products.data.push(...response.data); + this.products.meta = response.meta; + this.hasMore = response.meta.hasMore; + } catch (error) { + console.error(error); + } finally { + this.isLoading = false; + this.isLoadingMore = false; + this.isSearchPerformed = true; + } + }, }, }); diff --git a/frontend/spa/src/views/Search.vue b/frontend/spa/src/views/Search.vue index 42a85b2..0b6af1c 100644 --- a/frontend/spa/src/views/Search.vue +++ b/frontend/spa/src/views/Search.vue @@ -1,47 +1,55 @@ diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php index e33562e..12e0cd9 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ProductsHandler.php @@ -30,7 +30,7 @@ class ProductsHandler public function index(Request $request): JsonResponse { $page = (int) $request->json('page', 1); - $perPage = min((int) $request->json('perPage', 6), 15); + $perPage = min((int) $request->json('perPage', 3), 15); $maxPages = (int) $request->json('maxPages', 10); $search = trim($request->get('search', '')); $filters = $request->json('filters'); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php index 4e6816c..9b966e9 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php @@ -200,6 +200,7 @@ class ProductsService 'name' => Utils::htmlEntityEncode($product['product_name']), 'price' => $price, 'special' => $special, + 'image' => $image, 'images' => $allImages, 'special_numeric' => $specialPriceNumeric, 'price_numeric' => $priceNumeric, @@ -213,6 +214,7 @@ class ProductsService 'currentCategoryName' => $categoryName, 'hasMore' => $hasMore, 'debug' => $debug, + 'total' => $total, ] ]; }