schon/storefront/app/components/base/header/index.vue
Alexandr SaVBaD Waltz 8d7685ef67 feat(notification): integrate global notification plugin using ElNotification
Added a global `notify` method via Nuxt plugin to replace `useNotification`. Improved messaging structure by embedding progress bars and handled dynamic durations. Updated usage across composables and components for consistency.

- Replaced `useNotification` with `$notify` in all applicable files.
- Updated `app.config.ts` to support customizable notification positions.
- Refactored affected composables for simplified notification calls.
- Enhanced progress indicator display within notifications.

Breaking Changes:
`useNotification` is removed, requiring migration to the new `$notify` API.
2026-03-01 15:30:47 +03:00

371 lines
No EOL
8.4 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 />
<el-badge :value="productsInWishlistQuantity" color="#111827">
<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" color="#111827">
<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: rgba(255, 255, 255, 0.9);
border-bottom: 1px solid #f3f4f6;
&__no-search {
padding-inline: 75px;
& .header__inner {
gap: 75px;
}
}
&__fixed {
position: fixed;
}
&__wrapper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 25px;
padding-block: 25px;
}
&__inner {
width: 33%;
display: flex;
align-items: center;
justify-content: center;
&:first-child {
justify-content: flex-start;
}
&:last-child {
justify-content: flex-end;
}
}
&__logo {
transition: 0.2s;
font-size: 24px;
font-weight: 600;
letter-spacing: 6.7px;
font-family: 'Playfair Display', sans-serif;
color: #1a1a1a;
@include hover {
text-shadow: 0 0 5px #1a1a1a;
}
}
&__nav {
display: flex;
align-items: center;
gap: 40px;
&-item {
position: relative;
transition: 0.2s;
color: #1f2937;
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: #1f2937;
}
&.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;
transition: 0.2s;
color: #374151;
@include hover {
color: #29323f;
}
}
&-wishlist {
display: block;
cursor: pointer;
transition: 0.2s;
color: #374151;
@include hover {
color: #29323f;
}
}
&-cart {
display: block;
cursor: pointer;
transition: 0.2s;
color: #374151;
@include hover {
color: #29323f;
}
}
&-auth {
transition: 0.2s;
border-bottom: 1px solid #111827;
padding-bottom: 2px;
color: #111827;
font-size: 16px;
font-weight: 500;
letter-spacing: -0.5px;
@include hover {
color: #424c62;
}
}
&-avatar {
width: 28px;
border-radius: 50%;
border: 1px solid #374151;
transition: 0.2s;
& span {
display: block;
}
@include hover {
opacity: 0.7;
}
}
&-profile {
width: 28px;
border-radius: 50%;
padding: 5px;
border: 1px solid #374151;
transition: 0.2s;
& span {
display: block;
}
@include hover {
opacity: 0.7;
}
}
}
&__search {
position: absolute;
width: 100%;
top: 100%;
left: 0;
overflow: hidden;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.2s ease;
&.active {
grid-template-rows: 1fr;
}
& > * {
min-height: 0;
}
}
}
</style>