251 lines
No EOL
5.9 KiB
Vue
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> |