feat(storefront): enhance error handling and navigation across pages
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.
This commit is contained in:
parent
ef8be78d51
commit
c889c20b61
10 changed files with 156 additions and 16 deletions
|
|
@ -18,35 +18,35 @@
|
|||
<nuxt-link-locale
|
||||
to="/shop"
|
||||
class="header__nav-item"
|
||||
:class="[{ active: route.name.includes('shop') }]"
|
||||
: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') }]"
|
||||
: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') }]"
|
||||
: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') }]"
|
||||
: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') }]"
|
||||
:class="[{ active: route.name?.includes('contact') }]"
|
||||
>
|
||||
{{ t('header.nav.contact') }}
|
||||
</nuxt-link-locale>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
font-weight: 400;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
:deep(.title__wrapper div) {
|
||||
:deep(.title__wrapper .search) {
|
||||
position: relative;
|
||||
|
||||
& span {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ export async function useBrandBySlug(slug: string) {
|
|||
slug,
|
||||
});
|
||||
|
||||
if (!data.value?.brands?.edges?.length) {
|
||||
throw createError({
|
||||
status: 404,
|
||||
statusText: 'Brand not found',
|
||||
fatal: true
|
||||
});
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useBrandsBySlug error:', err);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ export async function useCategoryBySlug(slug: string) {
|
|||
});
|
||||
const minMaxPrices = computed(() => category.value?.minMaxPrices ?? { minPrice: 0, maxPrice: 50000 });
|
||||
|
||||
if (!data.value?.categories?.edges?.length) {
|
||||
throw createError({
|
||||
status: 404,
|
||||
statusText: 'Category not found',
|
||||
fatal: true
|
||||
});
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useCategoryBySlug error:', err);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ export async function useProductBySlug(slug: string) {
|
|||
product.value = result;
|
||||
}
|
||||
|
||||
if (!data.value?.products?.edges?.length) {
|
||||
throw createError({
|
||||
status: 404,
|
||||
statusText: 'Product not found',
|
||||
fatal: true
|
||||
});
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useProductBySlug error:', err);
|
||||
|
|
|
|||
82
storefront/app/error.vue
Normal file
82
storefront/app/error.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app';
|
||||
import {usePageTitle} from "@composables/utils";
|
||||
|
||||
const props = defineProps<{ error: NuxtError }>();
|
||||
|
||||
const {t} = useI18n();
|
||||
const localePath = useLocalePath();
|
||||
|
||||
const { setPageTitle } = usePageTitle();
|
||||
|
||||
const handleError = () => {
|
||||
clearError({ redirect: localePath('/') });
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
};
|
||||
|
||||
setPageTitle(props.error.status ?? t('errors.main'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="error">
|
||||
<div class="container">
|
||||
<client-only>
|
||||
<div class="error__wrapper">
|
||||
<h1 class="error__title">{{ error.status }}</h1>
|
||||
<h6 class="error__subtitle">{{ error.status === 404 ? t('errors.pageNotFound') : error.statusText }}</h6>
|
||||
<p class="error__text" v-if="error.status === 404">{{ t('errors.404') }}</p>
|
||||
<ui-button
|
||||
:type="'button'"
|
||||
class="error__button"
|
||||
@click="handleError"
|
||||
>
|
||||
{{ t('buttons.backToHome') }}
|
||||
</ui-button>
|
||||
</div>
|
||||
</client-only>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error {
|
||||
height: 100vh;
|
||||
background-color: $border;
|
||||
padding-block: 60px;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: "Playfair Display", sans-serif;
|
||||
color: $primary;
|
||||
font-size: 192px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-block: 30px 20px;
|
||||
font-family: "Playfair Display", sans-serif;
|
||||
color: $primary;
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $text;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin-top: 30px;
|
||||
padding-inline: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
:alt="brand.name"
|
||||
/>
|
||||
<h1>{{ brand.name }}</h1>
|
||||
<p>{{ brand.description }}</p>
|
||||
<div v-html="brand.description"></div>
|
||||
</ui-title>
|
||||
<div class="brand__main">
|
||||
<div class="container">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<ui-title>
|
||||
<h1>{{ t('brands.title') }}</h1>
|
||||
<p>{{ t('brands.text') }}</p>
|
||||
<div class="brands__top-search">
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="text"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
"sendMessage": "Send Message",
|
||||
"saveChanges": "Save Changes",
|
||||
"clearAll": "Clear All",
|
||||
"buyNow": "Buy Now"
|
||||
"buyNow": "Buy Now",
|
||||
"backToHome": "Back To Home"
|
||||
},
|
||||
"errors": {
|
||||
"required": "This field is required!",
|
||||
|
|
@ -32,7 +33,11 @@
|
|||
"needNumber": "Please include number.",
|
||||
"needMin": "Min. 8 characters",
|
||||
"needSpecial": "Please include a special character: #.?!$%^&*'()_+=:;\"'/>.<,|\\-",
|
||||
"pageNotFound": "Page not found"
|
||||
"pageNotFound": "Page not found",
|
||||
"404": "The page you're looking for doesn't exist or has been moved.",
|
||||
"brandNotFound": "Brand not found",
|
||||
"productNotFound": "Product not found",
|
||||
"categoryNotFound": "Category not found"
|
||||
},
|
||||
"fields": {
|
||||
"search": "Search",
|
||||
|
|
@ -49,7 +54,8 @@
|
|||
"confirmPassword": "Confirm password",
|
||||
"confirmNewPassword": "Confirm new password",
|
||||
"brandsSearch": "Search brands by name...",
|
||||
"promocode": "Enter promocode"
|
||||
"promocode": "Enter promocode",
|
||||
"address": "Start typing the address"
|
||||
},
|
||||
"checkboxes": {
|
||||
"remember": "Remember me",
|
||||
|
|
@ -72,6 +78,8 @@
|
|||
"reset": "If specified email exists in our system, we will send a password recovery email!",
|
||||
"newPassword": "You have successfully changed your password!",
|
||||
"contactUs": "Your message was sent successfully!",
|
||||
"createAddress": "You have successfully added a new address!",
|
||||
"deleteAddress": "You have successfully deleted an address!",
|
||||
"addToCart": "{product} has been added to the cart!",
|
||||
"removeFromCart": "{product} has been removed from the cart!",
|
||||
"removeAllFromCart": "You have successfully emptied the cart!",
|
||||
|
|
@ -196,7 +204,8 @@
|
|||
"return": "Return Policy",
|
||||
"faq": "FAQ",
|
||||
"shipping": "Shipping Information",
|
||||
"about": "About Us"
|
||||
"about": "About Us",
|
||||
"addresses": "Addresses"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Get in Touch",
|
||||
|
|
@ -280,6 +289,14 @@
|
|||
"until": "Until",
|
||||
"empty": "You don't have any promocodes."
|
||||
},
|
||||
"addresses": {
|
||||
"title": "Addresses",
|
||||
"title1": "Add New Address",
|
||||
"title2": "Your saved addresses",
|
||||
"search": {
|
||||
"empty": "Nothing found"
|
||||
}
|
||||
},
|
||||
"logout": "Logout"
|
||||
},
|
||||
"demo": {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
"sendMessage": "Отправить сообщение",
|
||||
"saveChanges": "Сохранить изменения",
|
||||
"clearAll": "Очистить всё",
|
||||
"buyNow": "Купить Сейчас"
|
||||
"buyNow": "Купить Сейчас",
|
||||
"backToHome": "Обратно на Главную"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Это поле обязательно!",
|
||||
|
|
@ -32,7 +33,11 @@
|
|||
"needNumber": "Добавьте цифру.",
|
||||
"needMin": "Мин. 8 символов",
|
||||
"needSpecial": "Добавьте спецсимвол: #.?!$%^&*'()_+=:;\"'/>.<,|\\-",
|
||||
"pageNotFound": "Страница не найдена"
|
||||
"pageNotFound": "Страница не найдена",
|
||||
"404": "Страница, которую вы ищете, не существует или была перемещена.",
|
||||
"brandNotFound": "Бренд не найден",
|
||||
"productNotFound": "Товар не найден",
|
||||
"categoryNotFound": "Категория не найдена"
|
||||
},
|
||||
"fields": {
|
||||
"search": "Поиск",
|
||||
|
|
@ -49,7 +54,8 @@
|
|||
"confirmPassword": "Подтвердите пароль",
|
||||
"confirmNewPassword": "Подтвердите новый пароль",
|
||||
"brandsSearch": "Поиск брендов по названию...",
|
||||
"promocode": "Введите промокод"
|
||||
"promocode": "Введите промокод",
|
||||
"address": "Начните вводить адрес"
|
||||
},
|
||||
"checkboxes": {
|
||||
"remember": "Запомнить меня",
|
||||
|
|
@ -72,6 +78,8 @@
|
|||
"reset": "Если указанный email существует в нашей системе, мы отправим на него письмо для восстановления пароля!",
|
||||
"newPassword": "Вы успешно изменили пароль!",
|
||||
"contactUs": "Ваше сообщение успешно отправлено!",
|
||||
"createAddress": "Вы успешно добавили новый адрес!",
|
||||
"deleteAddress": "Вы успешно удалили адрес!",
|
||||
"addToCart": "{product} добавлен в корзину!",
|
||||
"removeFromCart": "{product} удален из корзины!",
|
||||
"removeAllFromCart": "Корзина успешно очищена!",
|
||||
|
|
@ -196,7 +204,8 @@
|
|||
"return": "Политика возврата",
|
||||
"faq": "Часто задаваемые вопросы",
|
||||
"shipping": "Информация о доставке",
|
||||
"about": "О нас"
|
||||
"about": "О нас",
|
||||
"addresses": "Адреса"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Свяжитесь с нами",
|
||||
|
|
@ -280,6 +289,14 @@
|
|||
"until": "До",
|
||||
"empty": "У вас нет промокодов."
|
||||
},
|
||||
"addresses": {
|
||||
"title": "Адреса",
|
||||
"title1": "Добавить Новый Адрес",
|
||||
"title2": "Ваши сохраненные адреса",
|
||||
"search": {
|
||||
"empty": "Ничего не найдено"
|
||||
}
|
||||
},
|
||||
"logout": "Выйти"
|
||||
},
|
||||
"demo": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue