schon/storefront/app/pages/search.vue
Alexandr SaVBaD Waltz 556354a44d feat(storefront): overhaul theming system and unify SCSS variables
Revamped the theming system with new SCSS variables for consistent styling across light and dark themes. Replaced static color values with dynamic variables for maintainability and improved theme adaptability. Updated components and layouts to use the new variables.

- Moved theme plugin logic for optimized handling of theme cookies and attributes.
- Enhanced `useThemes` composable for simplified client-side updates and SSR support.
- Replaced redundant SCSS color definitions with centralized variables.
- Improved page structure by introducing `ui-title` for reusable section headers.
- Unified transitions and border-radius for consistent design language.

Breaking Changes:
Theming system restructured—migrate to `$main`, `$primary`, and related variables for SCSS colors. Remove usage of `--color-*` variables in templates and styles.
2026-03-01 20:16:05 +03:00

248 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: $primary_dark;
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: $disabled_secondary;
}
& input {
width: 100%;
border-radius: 12px;
border: 2px solid $border;
padding: 15px 20px;
color: $primary_dark;
font-size: 18px;
font-weight: 400;
letter-spacing: -0.5px;
&::placeholder {
color: $secondary_hover;
}
}
}
&__block {
width: 100%;
border-radius: $less_border_radius;
padding: 25px;
&-top {
display: flex;
align-items: center;
justify-content: space-between;
& h6 {
padding-bottom: 10px;
border-bottom: 2px solid $primary_dark;
font-family: "Playfair Display", sans-serif;
color: $primary_dark;
font-size: 24px;
font-weight: 600;
letter-spacing: -0.5px;
}
& button {
cursor: pointer;
display: grid;
place-items: center;
border-radius: $default_border_radius;
padding: 5px 12px;
background-color: transparent;
color: $primary_dark;
@include hover {
background-color: $main_hover;
}
& span {
&.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>