schon/storefront/app/pages/search.vue
2026-02-27 21:59:51 +03:00

251 lines
No EOL
5.9 KiB
Vue

<template>
<div class="search">
<div class="container">
<div class="search__wrapper">
<h1 class="search__title">{{ t('search.title') }}</h1>
<div class="search__input">
<input
type="text"
:placeholder="t('fields.search')"
v-model="searchInput"
>
<icon name="tabler:search" size="20" />
</div>
<template v-for="(block, idx) in blocks" :key="idx">
<div
class="search__block"
v-if="hasData(block.key)"
>
<div class="search__block-top">
<h6>{{ t(block.title) }} {{ t('search.byRequest') }} "{{ q }}"</h6>
<button @click="toggleBlock(block.key)">
<icon
name="material-symbols:keyboard-arrow-down-rounded"
size="24"
:class="{ rotated: !showBlocks[block.key] }"
/>
</button>
</div>
<div
class="search__list"
v-if="block.key === 'products'"
:class="[{active: showBlocks.products}]"
>
<div class="search__list-inner">
<cards-product
v-for="product in data?.products.edges"
:key="product.node.uuid"
:product="product.node"
/>
</div>
</div>
<div
class="search__list"
v-if="block.key === 'categories'"
:class="[{active: showBlocks.categories}]"
>
<div class="search__list-inner">
<cards-category
v-for="category in data?.categories.edges"
:key="category.node.uuid"
:category="category.node"
/>
</div>
</div>
<div
class="search__list"
v-if="block.key === 'brands'"
:class="[{active: showBlocks.brands}]"
>
<div class="search__list-inner">
<cards-brand
v-for="brand in data?.brands.edges"
:key="brand.node.uuid"
:brand="brand.node"
/>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {useRouteQuery} from '@vueuse/router';
import {usePageTitle} from '@composables/utils';
import {useSearchCombined} from '@composables/search';
import type {ISearchCombinedResponse} from '@types';
const {t} = useI18n();
const q = useRouteQuery('q', '');
const searchInput = ref<string>(q.value);
const { setPageTitle } = usePageTitle();
setPageTitle(t('breadcrumbs.search'));
const data = ref<ISearchCombinedResponse | null>(null);
watch(q, async (newQuery) => {
if (newQuery) {
const result = await useSearchCombined(newQuery);
data.value = result.data.value;
}
}, { immediate: true });
type SearchResponseKey = keyof ISearchCombinedResponse;
const updateNameDebounced = useDebounceFn(() => {
q.value = searchInput.value;
}, 500);
watch(searchInput, newVal => {
updateNameDebounced();
});
const showBlocks = reactive({
products: true,
categories: true,
brands: true,
});
const toggleBlock = (blockKey: SearchResponseKey) => {
showBlocks[blockKey] = !showBlocks[blockKey];
};
const blocks = computed(() => {
if (!data.value) return [];
return (Object.keys(data.value) as SearchResponseKey[])
.map((key) => ({
key,
title: `search.${key}`
}));
});
const hasData = (blockKey: string): boolean => {
const validKey = blockKey as SearchResponseKey;
return (data.value?.[validKey]?.edges?.length ?? 0) > 0;
};
</script>
<style scoped lang="scss">
.search {
padding-block: 75px;
&__wrapper {
display: flex;
flex-direction: column;
gap: 50px;
}
&__title {
color: #1a1a1a;
font-family: "Playfair Display", sans-serif;
font-size: 30px;
font-weight: 600;
letter-spacing: -0.5px;
}
&__input {
width: 400px;
position: relative;
& span {
position: absolute;
top: 50%;
right: 24px;
transform: translateY(-50%);
color: #8c8c8c;
}
& input {
width: 100%;
border-radius: 12px;
border: 2px solid #e5e7eb;
padding: 15px 20px;
color: $black;
font-size: 18px;
font-weight: 400;
letter-spacing: -0.5px;
&::placeholder {
color: #444444;
}
}
}
&__block {
width: 100%;
border-radius: $default_border_radius;
padding: 25px;
&-top {
display: flex;
align-items: center;
justify-content: space-between;
& h6 {
padding-bottom: 10px;
border-bottom: 2px solid $accentDark;
font-family: "Playfair Display", sans-serif;
color: #1a1a1a;
font-size: 24px;
font-weight: 600;
letter-spacing: -0.5px;
}
& button {
cursor: pointer;
display: grid;
place-items: center;
border-radius: 8px;
padding: 5px 12px;
background-color: transparent;
transition: 0.2s;
color: #000;
@include hover {
background-color: #dcdcdc;
}
& span {
transition: 0.2s;
&.rotated {
transform: rotate(-180deg);
}
}
}
}
}
&__list {
overflow: hidden;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.2s ease;
&.active {
grid-template-rows: 1fr;
}
& > * {
min-height: 0;
}
&-inner {
margin-top: 25px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(275px, 275px));
justify-content: space-between;
column-gap: 20px;
row-gap: 40px;
}
}
}
</style>