Added a global error page to improve user experience during navigation issues, with localized messages and redirect options. Enhanced error handling for brand, product, and category slug composables by introducing explicit 404 responses. - Introduced `/error.vue` template for custom error displays using `NuxtError`. - Updated `useBrandBySlug`, `useProductBySlug`, `useCategoryBySlug` to throw 404 errors when data is not found. - Expanded i18n files (`en-gb.json` and `ru-ru.json`) with additional error-related translations. - Replaced plain text input with a `.search`-scoped class for cleaner styling. Enhances robustness and user feedback during navigation errors. No breaking changes introduced.
362 lines
No EOL
8.3 KiB
Vue
362 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
|
|
v-if="uiConfig.showSearchBar"
|
|
@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> |