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:
Alexandr SaVBaD Waltz 2026-03-04 16:27:52 +03:00
parent ef8be78d51
commit c889c20b61
10 changed files with 156 additions and 16 deletions

View file

@ -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>

View file

@ -40,7 +40,7 @@
font-weight: 400;
letter-spacing: -0.5px;
}
:deep(.title__wrapper div) {
:deep(.title__wrapper .search) {
position: relative;
& span {

View file

@ -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);

View file

@ -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);

View file

@ -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
View 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>

View file

@ -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">

View file

@ -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"

View file

@ -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": {

View file

@ -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": {