schon/storefront/app/components/base/header/index.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

361 lines
No EOL
8.3 KiB
Vue

<template>
<header
class="header"
:class="[{
'header__no-search': !uiConfig.showSearchBar,
'header__fixed': uiConfig.isHeaderFixed
}]"
>
<div class="container">
<div class="header__wrapper">
<div class="header__inner">
<nuxt-link-locale to="/" class="header__logo">
SCHON
</nuxt-link-locale>
</div>
<div class="header__inner">
<nav class="header__nav">
<nuxt-link-locale
to="/shop"
class="header__nav-item"
:class="[{ active: route.name.includes('shop') }]"
>
{{ t('header.nav.shop') }}
</nuxt-link-locale>
<nuxt-link-locale
to="/catalog"
class="header__nav-item"
:class="[{ active: route.name.includes('catalog') }]"
>
{{ t('header.nav.catalog') }}
</nuxt-link-locale>
<nuxt-link-locale
to="/brands"
class="header__nav-item"
:class="[{ active: route.name.includes('brands') }]"
>
{{ t('header.nav.brands') }}
</nuxt-link-locale>
<nuxt-link-locale
to="/blog"
class="header__nav-item"
:class="[{ active: route.name.includes('blog') }]"
>
{{ t('header.nav.blog') }}
</nuxt-link-locale>
<nuxt-link-locale
to="/contact"
class="header__nav-item"
:class="[{ active: route.name.includes('contact') }]"
>
{{ t('header.nav.contact') }}
</nuxt-link-locale>
</nav>
</div>
<div class="header__inner">
<div class="header__block">
<icon
@click="isSearchVisible = true"
class="header__block-search"
name="tabler:search"
size="20"
/>
<ui-language-switcher />
<ui-theme-toggle />
<el-badge :value="productsInWishlistQuantity">
<nuxt-link-locale to="/wishlist">
<icon class="header__block-wishlist" name="material-symbols:favorite-rounded" size="20" />
</nuxt-link-locale>
</el-badge>
<el-badge :value="productsInCartQuantity">
<nuxt-link-locale to="/cart">
<icon class="header__block-cart" name="bx:bxs-shopping-bag" size="20" />
</nuxt-link-locale>
</el-badge>
<nuxt-link-locale
to="/profile/settings"
class="header__block-item"
v-if="isAuthenticated"
>
<nuxt-img
class="header__block-avatar"
v-if="user?.avatar"
:src="user?.avatar"
alt="avatar"
format="webp"
densities="x1"
/>
<div class="header__block-profile" v-else>
<icon name="clarity:avatar-line" size="16" />
</div>
</nuxt-link-locale>
<nuxt-link-locale
to="/auth/sign-in"
class="header__block-auth"
v-else
>
<p>{{ t('buttons.login') }}</p>
</nuxt-link-locale>
</div>
</div>
</div>
</div>
<div class="header__search" :class="[{ active: isSearchVisible && uiConfig.showSearchBar }]">
<ui-search
ref="searchRef"
/>
</div>
</header>
</template>
<script setup lang="ts">
import { useProjectConfig } from "@composables/config";
import {onClickOutside} from "@vueuse/core";
const { t } = useI18n();
const localePath = useLocalePath();
const route = useRoute();
const appStore = useAppStore();
const userStore = useUserStore();
const wishlistStore = useWishlistStore();
const cartStore = useCartStore();
const { $appHelpers } = useNuxtApp();
const { uiConfig } = useProjectConfig();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const user = computed(() => userStore.user);
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
default: () => [],
path: '/',
});
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
default: () => [],
path: '/',
});
const productsInCartQuantity = computed(() => {
if (isAuthenticated.value) {
let count = 0;
cartStore.currentOrder?.orderProducts?.edges.forEach((el) => {
count = count + el.node.quantity;
});
return count;
} else {
return cookieCart.value.reduce((acc, item) => acc + item.quantity, 0);
}
});
const productsInWishlistQuantity = computed(() => {
if (isAuthenticated.value) {
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges.length : 0;
} else {
return cookieWishlist.value.length
}
});
const isSearchVisible = ref<boolean>(false);
const searchRef = ref(null);
onClickOutside(searchRef, () => isSearchVisible.value = false);
const redirectTo = (to) => {
if (uiConfig.value.isAuthModals) {
appStore.setActiveAuthState(to);
} else {
navigateTo(localePath(`/auth/ + ${to}`));
}
};
</script>
<style lang="scss" scoped>
.header {
position: relative;
z-index: 5;
top: 0;
left: 0;
width: 100vw;
background-color: $main;
border-bottom: 1px solid $border;
&__fixed {
position: fixed;
}
&__wrapper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 25px;
padding-block: 25px;
background-color: $main;
}
&__inner {
width: 33%;
display: flex;
align-items: center;
justify-content: center;
&:first-child {
justify-content: flex-start;
}
&:last-child {
justify-content: flex-end;
}
}
&__logo {
font-size: 24px;
font-weight: 600;
letter-spacing: 6.7px;
font-family: 'Playfair Display', sans-serif;
color: $primary_dark;
@include hover {
text-shadow: 0 0 5px $primary_dark;
}
}
&__nav {
display: flex;
align-items: center;
gap: 40px;
&-item {
position: relative;
color: $link_primary;
font-size: 16px;
font-weight: 500;
letter-spacing: -0.5px;
&::after {
content: "";
position: absolute;
bottom: -3px;
left: 0;
height: 2px;
width: 0;
transition: all .3s ease;
background-color: $link_primary;
}
&.active::after {
width: 100%;
}
@include hover {
&::after {
width: 100%;
}
}
}
}
&__block {
display: flex;
align-items: center;
gap: 16px;
&-block {
display: flex;
align-items: center;
gap: 15px;
}
&-search {
cursor: pointer;
display: block;
color: $secondary;
@include hover {
color: $secondary_hover;
}
}
&-wishlist {
display: block;
cursor: pointer;
color: $secondary;
@include hover {
color: $secondary_hover;
}
}
&-cart {
display: block;
cursor: pointer;
color: $secondary;
@include hover {
color: $secondary_hover;
}
}
&-auth {
border-bottom: 1px solid $secondary;
padding-bottom: 2px;
color: $secondary;
font-size: 16px;
font-weight: 500;
letter-spacing: -0.5px;
@include hover {
color: $secondary_hover;
}
}
&-avatar {
width: 28px;
border-radius: 50%;
border: 1px solid $secondary;
& span {
display: block;
}
@include hover {
opacity: 0.7;
}
}
&-profile {
width: 28px;
border-radius: 50%;
padding: 5px;
border: 1px solid $secondary;
& span {
color: $primary;
display: block;
}
@include hover {
opacity: 0.7;
}
}
}
&__search {
position: relative;
width: 100%;
top: 100%;
left: 0;
display: grid;
grid-template-rows: 0fr;
//transition: grid-template-rows 0.2s ease;
opacity: 0;
visibility: hidden;
&.active {
grid-template-rows: 1fr;
opacity: 1;
visibility: visible;
}
& > * {
min-height: 0;
}
}
}
</style>