Compare commits

...

3 commits

Author SHA1 Message Date
c36135d78d feat(storefront): add wishlist and guest cart support with cookie persistence
Enhancements:
- Introduced `wishlist.vue` for displaying and managing the wishlist.
- Added guest cart and wishlist handling via cookies for unauthenticated users.
- Implemented synchronization logic for wishlist and cart (`useOrderSync` and `useWishlistSync`) upon user login.
- Updated `cart.vue` layout with a bulk 'add all to cart' button for wishlist items.
- Enhanced `post.vue` prop handling for improved type safety.

Fixes:
- Fixed breadcrumbs console log removal in `useBreadcrumbs.ts`.
- Corrected and unified translations in `en-gb.json` for cart and wishlist descriptions.
- Fixed stale routes in footer (`terms-and-condition` -> `terms-and-conditions`, etc.).

Extras:
- Refactored composables `useWishlistOverwrite` and `useOrderOverwrite` for cookie-based fallback.
- Applied code styling improvements, organized imports, and optimized API requests in Apollo plugin.
2026-02-28 17:39:17 +03:00
8c4ec23f92 Merge branch 'master' into storefront-nuxt 2026-02-28 09:26:38 +03:00
ce689ee754 feat(serializers): add min_price and max_price fields in detail serializer
ensure pricing data is included in the serialized output for improved filtering capabilities.
2026-02-28 02:29:12 +03:00
23 changed files with 1045 additions and 413 deletions

View file

@ -78,6 +78,8 @@ class CategoryDetailSerializer(ModelSerializer):
"markup_percent",
"filterable_attributes",
"brands",
"min_price",
"max_price",
"children",
"slug",
"created",

View file

@ -20,9 +20,9 @@
<h6>{{ t('footer.help') }}</h6>
<nuxt-link-locale to="/contact">{{ t('contact.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/privacy-policy">{{ t('docs.policy.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/terms-and-condition">{{ t('docs.terms.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/terms-and-conditions">{{ t('docs.terms.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/shipping-information">{{ t('docs.shipping.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/refund-policy">{{ t('docs.return.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/return-policy">{{ t('docs.return.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/about-us">{{ t('docs.about.title') }}</nuxt-link-locale>
<nuxt-link-locale to="/docs/faq">{{ t('docs.faq.title') }}</nuxt-link-locale>
</div>

View file

@ -119,22 +119,35 @@ 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 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);
@ -302,7 +315,7 @@ const redirectTo = (to) => {
}
&-avatar {
width: 32px;
width: 28px;
border-radius: 50%;
border: 1px solid #374151;
transition: 0.2s;
@ -317,7 +330,7 @@ const redirectTo = (to) => {
}
&-profile {
width: 32px;
width: 28px;
border-radius: 50%;
padding: 5px;
border: 1px solid #374151;

View file

@ -9,7 +9,7 @@
import type {IPost} from '@types';
const props = defineProps<{
post: { node: IPost }[];
post: IPost;
}>();
const {t} = useI18n();

View file

@ -7,11 +7,10 @@
class="card__wishlist"
@click="overwriteWishlist({
type: (isProductInWishlist ? 'remove' : 'add'),
productUuid: product.uuid,
productName: product.name
product: product,
})"
>
<icon name="mdi:cards-heart" size="16" v-if="isProductInWishlist" />
<icon style="color: #dc2626;" name="mdi:cards-heart" size="16" v-if="isProductInWishlist" />
<icon name="mdi:cards-heart-outline" size="16" v-else />
</div>
<div class="card__wrapper">

View file

@ -43,6 +43,8 @@ export function useLogin() {
userStore.setUser(authData.user);
cookieAccess.value = authData.accessToken;
await nextTick();
navigateTo(localePath('/'));
useNotification({

View file

@ -82,8 +82,6 @@ export function useBreadcrumbs() {
return crumbs;
});
console.log(breadcrumbs.value)
return {
breadcrumbs,
};

View file

@ -1,3 +1,4 @@
export * from './useOrderBuy';
export * from './useOrderOverwrite';
export * from './useOrderSync';
export * from './useOrders';

View file

@ -8,7 +8,7 @@ import {
} from '@graphql/mutations/cart';
import type {
IAddToOrderResponse,
IBulkOrderResponse,
IBulkOrderResponse, IProduct,
IRemoveAllFromOrderResponse,
IRemoveFromOrderResponse,
IRemoveKindFromOrderResponse,
@ -16,9 +16,9 @@ import type {
interface IOverwriteOrderArguments {
type: string;
productUuid?: string;
productName?: string;
product: IProduct;
bulkAction?: string;
isBulkSync?: boolean;
products?: {
uuid: string;
}[];
@ -32,6 +32,11 @@ export function useOrderOverwrite() {
const isAuthenticated = computed(() => userStore.isAuthenticated);
const orderUuid = computed(() => cartStore.currentOrder?.uuid);
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
default: () => [],
path: '/',
});
const {
mutate: addMutate,
loading: addLoading,
@ -64,7 +69,7 @@ export function useOrderOverwrite() {
case 'add': {
const addResult = await addMutate({
orderUuid: orderUuid.value,
productUuid: args.productUuid,
productUuid: args.product.uuid,
});
if (addResult?.data?.addOrderProduct?.order) {
@ -72,7 +77,7 @@ export function useOrderOverwrite() {
useNotification({
message: t('popup.success.addToCart', {
product: args.productName,
product: args.product.name,
}),
type: 'success',
});
@ -84,7 +89,7 @@ export function useOrderOverwrite() {
case 'remove': {
const removeResult = await removeMutate({
orderUuid: orderUuid.value,
productUuid: args.productUuid,
productUuid: args.product.uuid,
});
if (removeResult?.data?.removeOrderProduct?.order) {
@ -92,7 +97,7 @@ export function useOrderOverwrite() {
useNotification({
message: t('popup.success.removeFromCart', {
product: args.productName,
product: args.product.name,
}),
type: 'success',
});
@ -104,7 +109,7 @@ export function useOrderOverwrite() {
case 'removeKind': {
const removeKindResult = await removeKindMutate({
orderUuid: orderUuid.value,
productUuid: args.productUuid,
productUuid: args.product.uuid,
});
if (removeKindResult?.data?.removeOrderProductsOfAKind?.order) {
@ -112,7 +117,7 @@ export function useOrderOverwrite() {
useNotification({
message: t('popup.success.removeFromCart', {
product: args.productName,
product: args.product.name,
}),
type: 'success',
});
@ -148,7 +153,7 @@ export function useOrderOverwrite() {
if (bulkResult?.data?.bulkOrderAction?.order) {
cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order);
useNotification({
message: t('popup.success.bulkRemoveWishlist'),
message: t('popup.success.bulkRemoveOrder'),
type: 'success',
});
}
@ -160,10 +165,119 @@ export function useOrderOverwrite() {
console.error('No type provided for overwriteOrder');
}
} else {
switch (args.type) {
case 'add': {
const currentCart = cookieCart.value || [];
const existingItem = currentCart.find(
(item) => item.product.uuid === args.product.uuid
);
if (existingItem) {
existingItem.quantity += 1;
cookieCart.value = [...currentCart];
} else {
cookieCart.value = [
...currentCart,
{ product: args.product, quantity: 1 }
];
}
useNotification({
message: t('popup.errors.loginFirst'),
type: 'error',
message: t('popup.success.addToCart', {
product: args.product.name,
}),
type: 'success',
});
break;
}
case 'remove': {
const currentCart = cookieCart.value || [];
const existingItem = currentCart.find(
(item) => item.product.uuid === args.product.uuid
);
if (existingItem) {
if (existingItem.quantity > 1) {
existingItem.quantity -= 1;
cookieCart.value = [...currentCart];
} else {
cookieCart.value = currentCart.filter(
(item) => item.product.uuid !== args.product.uuid
);
}
useNotification({
message: t('popup.success.removeFromCart', {
product: args.product.name,
}),
type: 'success',
});
}
break;
}
case 'removeKind': {
cookieCart.value = cookieCart.value.filter(
(item) => item.product.uuid !== args.product.uuid
);
useNotification({
message: t('popup.success.removeFromCart', {
product: args.product.name,
}),
type: 'success',
});
break;
}
case 'removeAll': {
cookieCart.value = [];
useNotification({
message: t('popup.success.removeAllFromCart'),
type: 'success',
});
break;
}
case 'bulk': {
if (args.bulkAction === 'remove' && args.products) {
const uuidsToRemove = args.products.map(p => p.uuid);
cookieCart.value = cookieCart.value.filter(
(item) => !uuidsToRemove.includes(item.product.uuid)
);
useNotification({
message: t('popup.success.bulkRemoveOrder'),
type: 'success',
});
} else if (args.bulkAction === 'add' && args.products) {
const currentCart = cookieCart.value || [];
for (const productRef of args.products) {
const existingItem = currentCart.find(
(item) => item.product.uuid === productRef.uuid
);
if (existingItem) {
existingItem.quantity += 1;
}
}
cookieCart.value = [...currentCart];
}
break;
}
default:
console.error('No type provided for overwriteOrder');
}
}
}

View file

@ -0,0 +1,68 @@
import { useOrderOverwrite } from '@composables/orders/useOrderOverwrite';
export function useOrderSync() {
const cartStore = useCartStore();
const userStore = useUserStore();
const { $appHelpers } = useNuxtApp();
const { overwriteOrder } = useOrderOverwrite();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const orderUuid = computed(() => cartStore.currentOrder?.uuid);
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
default: () => [],
path: '/',
});
async function syncOrder() {
if (!isAuthenticated.value || !orderUuid.value) {
return;
}
const cookieCartItems = cookieCart.value || [];
if (cookieCartItems.length === 0) {
return;
}
const apiCartProducts = cartStore.currentOrder?.orderProducts?.edges || [];
const apiProductMap = new Map(
apiCartProducts.map(e => [e.node.product.uuid, e.node.quantity])
);
const productsToSync = [];
for (const cartItem of cookieCartItems) {
const apiQuantity = apiProductMap.get(cartItem.product.uuid) || 0;
const quantityDifference = cartItem.quantity - apiQuantity;
if (quantityDifference > 0) {
for (let i = 0; i < quantityDifference; i++) {
productsToSync.push({ uuid: cartItem.product.uuid });
}
}
}
if (productsToSync.length === 0) {
cookieCart.value = [];
return;
}
try {
await overwriteOrder({
type: 'bulk',
bulkAction: 'add',
isBulkSync: true,
products: productsToSync
});
} catch (err) {
console.error('Failed to sync cart:', err);
}
}
return {
syncOrder,
};
}

View file

@ -1,4 +1,6 @@
import { orderStatuses } from '@appConstants';
import { useOrderSync } from '@composables/orders';
import {useWishlistSync} from "@composables/wishlist";
import { getUserBaseData } from '@graphql/queries/combined/userBaseData';
import type { IUserBaseDataResponse } from '@types';
@ -7,6 +9,9 @@ export async function useUserBaseData(userEmail: string) {
const cartStore = useCartStore();
const promocodeStore = usePromocodeStore();
const { syncWishlist } = useWishlistSync();
const { syncOrder } = useOrderSync();
const { document, variables } = getUserBaseData({
userEmail,
status: orderStatuses.PENDING,
@ -19,9 +24,13 @@ export async function useUserBaseData(userEmail: string) {
if (data?.wishlists.edges) {
wishlistStore.setWishlist(data.wishlists.edges[0].node);
await syncWishlist();
}
if (data?.orders.edges) {
cartStore.setCurrentOrders(data.orders.edges[0].node);
await syncOrder();
}
if (data?.promocodes.edges) {
promocodeStore.setPromocodes(data.promocodes.edges);

View file

@ -1,2 +1,3 @@
export * from './useWishlist';
export * from './useWishlistOverwrite';
export * from './useWishlistSync';

View file

@ -7,16 +7,16 @@ import {
} from '@graphql/mutations/wishlist';
import type {
IAddToWishlistResponse,
IBulkWishlistResponse,
IBulkWishlistResponse, IProduct,
IRemoveAllFromWishlistResponse,
IRemoveFromWishlistResponse,
} from '@types';
interface IOverwriteWishlistArguments {
type: string;
productUuid?: string;
productName?: string;
product: IProduct;
bulkAction?: string;
isBulkSync?: boolean;
products?: {
uuid: string;
}[];
@ -26,10 +26,16 @@ export function useWishlistOverwrite() {
const { t } = useI18n();
const wishlistStore = useWishlistStore();
const userStore = useUserStore();
const { $appHelpers } = useNuxtApp();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const wishlistUuid = computed(() => wishlistStore.wishlist?.uuid);
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
default: () => [],
path: '/',
});
const {
mutate: addMutate,
loading: addLoading,
@ -57,7 +63,7 @@ export function useWishlistOverwrite() {
case 'add': {
const addResult = await addMutate({
wishlistUuid: wishlistUuid.value,
productUuid: args.productUuid,
productUuid: args.product.uuid,
});
if (addResult?.data?.addWishlistProduct?.wishlist) {
@ -65,7 +71,7 @@ export function useWishlistOverwrite() {
useNotification({
message: t('popup.success.addToWishlist', {
product: args.productName,
product: args.product.name,
}),
type: 'success',
});
@ -77,7 +83,7 @@ export function useWishlistOverwrite() {
case 'remove': {
const removeResult = await removeMutate({
wishlistUuid: wishlistUuid.value,
productUuid: args.productUuid,
productUuid: args.product.uuid,
});
if (removeResult?.data?.removeWishlistProduct?.wishlist) {
@ -85,7 +91,7 @@ export function useWishlistOverwrite() {
useNotification({
message: t('popup.success.removeFromWishlist', {
product: args.productName,
product: args.product.name,
}),
type: 'success',
});
@ -97,7 +103,7 @@ export function useWishlistOverwrite() {
case 'removeAll': {
const removeAllResult = await removeAllMutate({
wishlistUuid: wishlistUuid.value,
productUuid: args.productUuid,
productUuid: args.product.uuid,
});
if (removeAllResult?.data?.removeAllWishlistProducts?.wishlist) {
@ -121,11 +127,16 @@ export function useWishlistOverwrite() {
if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist);
if (args.isBulkSync) {
cookieWishlist.value = [];
} else {
useNotification({
message: t('popup.success.bulkRemoveWishlist'),
type: 'success',
});
}
}
break;
}
@ -134,10 +145,85 @@ export function useWishlistOverwrite() {
console.error('No type provided for overwriteWishlist');
}
} else {
switch (args.type) {
case 'add': {
const isAlreadyInWishlist = cookieWishlist.value.some(
(item) => item.uuid === args.product.uuid
);
if (isAlreadyInWishlist) {
useNotification({
message: t('popup.errors.loginFirst'),
type: 'error',
message: t('popup.errors.alreadyInWishlist', {
product: args.product.name,
}),
type: 'warning',
});
} else {
cookieWishlist.value = [...cookieWishlist.value, args.product];
useNotification({
message: t('popup.success.addToWishlist', {
product: args.product.name,
}),
type: 'success',
});
}
break;
}
case 'remove': {
cookieWishlist.value = cookieWishlist.value.filter(
(item) => item.uuid !== args.product.uuid
);
useNotification({
message: t('popup.success.removeFromWishlist', {
product: args.product.name,
}),
type: 'success',
});
break;
}
case 'removeAll': {
cookieWishlist.value = [];
useNotification({
message: t('popup.success.removeAllFromWishlist'),
type: 'success',
});
break;
}
case 'bulk': {
const bulkResult = await bulkMutate({
wishlistUuid: wishlistUuid.value,
action: args.bulkAction,
products: args.products,
});
if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist);
if (args.isBulkSync) {
cookieWishlist.value = [];
} else {
useNotification({
message: t('popup.success.bulkRemoveWishlist'),
type: 'success',
});
}
}
break;
}
default:
console.error('No type provided for overwriteWishlist');
}
}
}

View file

@ -0,0 +1,66 @@
import {useWishlistOverwrite} from "@composables/wishlist/useWishlistOverwrite";
export function useWishlistSync() {
const wishlistStore = useWishlistStore();
const userStore = useUserStore();
const { $appHelpers } = useNuxtApp();
const { overwriteWishlist } = useWishlistOverwrite();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const wishlistUuid = computed(() => wishlistStore.wishlist?.uuid);
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
default: () => [],
path: '/',
});
async function syncWishlist() {
if (!isAuthenticated.value || !wishlistUuid.value) {
return;
}
const cookieProducts = cookieWishlist.value || [];
if (cookieProducts.length === 0) {
return;
}
const apiProductUuids = wishlistStore.wishlist?.products?.edges.map(e => e.node.uuid) || [];
const productsToAdd = cookieProducts.filter(
(product) => !apiProductUuids.includes(product.uuid)
);
if (productsToAdd.length === 0) {
cookieWishlist.value = [];
return;
}
try {
await overwriteWishlist({
type: 'bulk',
bulkAction: 'add',
isBulkSync: true,
products: productsToAdd.map(p => ({ uuid: p.uuid }))
})
if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist);
cookieWishlist.value = [];
}
} catch (err) {
console.error('Failed to sync wishlist:', err);
}
}
watch(syncError, (err) => {
if (!err) return;
console.error('useWishlistSync error:', err);
});
return {
syncWishlist,
};
}

View file

@ -2,6 +2,8 @@ export const COOKIE_KEY_TEMPLATES = {
LOCALE: (appNameKey: string) => `${appNameKey}-locale`,
REFRESH_TOKEN: (appNameKey: string) => `${appNameKey}-refresh`,
ACCESS_TOKEN: (appNameKey: string) => `${appNameKey}-access`,
WISHLIST_TOKEN: (appNameKey: string) => `${appNameKey}-wishlist`,
CART_TOKEN: (appNameKey: string) => `${appNameKey}-cart`,
PRODUCT_VIEW: (appNameKey: string) => `${appNameKey}-product-view`,
THEME: (appNameKey: string) => `${appNameKey}-theme`,
} as const;

View file

@ -4,7 +4,27 @@
<div class="cart__wrapper">
<div class="cart__top">
<h1 class="cart__top-title">{{ t('cart.title') }}</h1>
<div class="cart__top-inner">
<p>({{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }})</p>
<ui-button
:type="'button'"
class="cart__top-button"
@click="buyOrder"
>
<icon name="material-symbols:add" size="20" />
{{ t('buttons.checkout') }}
</ui-button>
</div>
</div>
<div class="cart__list">
<div class="cart__list-inner" v-if="productsInCart.length">
<cards-product
v-for="product in productsInCart"
:key="product.node.uuid"
:product="product.node.product"
/>
</div>
<p class="cart__empty" v-else>{{ t('cart.empty') }}</p>
</div>
</div>
</div>
@ -13,23 +33,55 @@
<script setup lang="ts">
import {usePageTitle} from "@composables/utils";
import {useOrderBuy} from "~/composables/orders";
const {t} = useI18n();
const cartStore = useCartStore();
const userStore = useUserStore();
const { $appHelpers } = useNuxtApp();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
default: () => [],
path: '/',
});
const { buyOrder } = useOrderBuy();
const productsInCart = computed(() => {
if (isAuthenticated.value) {
return cartStore.currentOrder ? cartStore.currentOrder.orderProducts.edges : [];
} else {
return cookieCart.value.map(product => ({
node: {
product: item.product,
quantity: item.quantity
}
}));
}
});
const totalPrice = computed(() => {
if (isAuthenticated.value) {
return cartStore.currentOrder ? cartStore.currentOrder.totalPrice : 0;
} else {
return cookieCart.value.reduce((acc, item) => {
return acc + (item.product.price * item.quantity);
}, 0);
}
});
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 { setPageTitle } = usePageTitle();
@ -45,7 +97,7 @@ setPageTitle(t('breadcrumbs.cart'));
&__wrapper {
display: flex;
flex-direction: column;
gap: 32px;
gap: 50px;
}
&__top {
@ -53,6 +105,12 @@ setPageTitle(t('breadcrumbs.cart'));
align-items: center;
justify-content: space-between;
&-inner {
display: flex;
align-items: center;
gap: 35px;
}
&-title {
color: #1a1a1a;
font-family: "Playfair Display", sans-serif;
@ -67,6 +125,19 @@ setPageTitle(t('breadcrumbs.cart'));
font-weight: 400;
letter-spacing: -0.5px;
}
&-button {
width: fit-content;
padding-inline: 25px;
}
}
&__list {
&-inner {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 25px;
}
}
}
</style>

View file

@ -79,8 +79,7 @@
class="product__main-button"
@click="overwriteWishlist({
type: (isProductInWishlist ? 'remove' : 'add'),
productUuid: product.uuid,
productName: product.name
product: product
})"
:type="'button'"
:style="'secondary'"

View file

@ -0,0 +1,158 @@
<template>
<div class="wishlist">
<div class="container">
<div class="wishlist__wrapper">
<div class="wishlist__top">
<h1 class="wishlist__top-title">{{ t('wishlist.title') }}</h1>
<div class="wishlist__top-inner">
<p>({{ t('wishlist.items', productsInWishlist.length, { count: productsInWishlist.length }) }})</p>
<ui-button
:type="'button'"
class="wishlist__top-button"
@click="onBulkAdd"
:isLoading="bulkLoading"
>
<icon name="material-symbols:add" size="20" />
{{ t('buttons.addAllToCart') }}
</ui-button>
</div>
</div>
<div class="wishlist__list">
<div class="wishlist__list-inner" v-if="productsInWishlist.length">
<cards-product
v-for="product in productsInWishlist"
:key="product.node.uuid"
:product="product.node"
/>
</div>
<p class="wishlist__empty" v-else>{{ t('wishlist.empty') }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {usePageTitle} from '@composables/utils';
import {useWishlistOverwrite} from '@composables/wishlist';
import {useOrderOverwrite} from "@composables/orders";
const {t} = useI18n();
const wishlistStore = useWishlistStore();
const userStore = useUserStore();
const { $appHelpers } = useNuxtApp();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
default: () => [],
path: '/',
});
const { overwriteWishlist } = useWishlistOverwrite();
const { overwriteOrder, bulkLoading } = useOrderOverwrite();
const productsInWishlist = computed(() => {
if (isAuthenticated.value) {
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges : [];
} else {
return cookieWishlist.value.map(product => ({
node: product
}));
}
});
const totalPrice = computed(() => {
return productsInWishlist.value.reduce((acc, p) => acc + p.node.price, 0);
});
const selectedProducts = ref<{ uuid: string }[]>([]);
const productsUuid = computed<boolean>(() => {
return productsInWishlist.value.map(p => ({ uuid: p.node.uuid }));
});
function toggleUuid(uuid: string, checked: boolean) {
if (checked) {
if (!selectedProducts.value.some(o => o.uuid === uuid)) {
selectedProducts.value.push({ uuid });
}
}
else {
selectedProducts.value = selectedProducts.value
.filter(o => o.uuid !== uuid);
}
}
function onBulkAdd() {
overwriteOrder({
type: 'bulk',
bulkAction: 'add',
products: productsUuid.value
});
}
function onBulkRemove() {
overwriteWishlist({
type: 'bulkRemove',
bulkAction: 'remove',
products: productsUuid.value
});
}
const { setPageTitle } = usePageTitle();
setPageTitle(t('breadcrumbs.wishlist'));
</script>
<style lang="scss" scoped>
.wishlist {
padding-block: 50px 100px;
background-color: $white;
&__wrapper {
display: flex;
flex-direction: column;
gap: 50px;
}
&__top {
display: flex;
align-items: center;
justify-content: space-between;
&-title {
color: #1a1a1a;
font-family: "Playfair Display", sans-serif;
font-size: 36px;
font-weight: 600;
letter-spacing: -0.5px;
}
&-inner {
display: flex;
align-items: center;
gap: 35px;
}
& p {
color: #4b5563;
font-size: 18px;
font-weight: 400;
letter-spacing: -0.5px;
}
&-button {
width: fit-content;
padding-inline: 25px;
}
}
&__list {
&-inner {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 25px;
}
}
}
</style>

View file

@ -14,6 +14,8 @@ export default defineNuxtPlugin(() => {
COOKIES_LOCALE_KEY: COOKIE_KEY_TEMPLATES.LOCALE(APP_NAME_KEY),
COOKIES_REFRESH_TOKEN_KEY: COOKIE_KEY_TEMPLATES.REFRESH_TOKEN(APP_NAME_KEY),
COOKIES_ACCESS_TOKEN_KEY: COOKIE_KEY_TEMPLATES.ACCESS_TOKEN(APP_NAME_KEY),
COOKIES_WISHLIST_KEY: COOKIE_KEY_TEMPLATES.WISHLIST_TOKEN(APP_NAME_KEY),
COOKIES_CART_KEY: COOKIE_KEY_TEMPLATES.CART_TOKEN(APP_NAME_KEY),
COOKIES_PRODUCT_VIEW_KEY: COOKIE_KEY_TEMPLATES.PRODUCT_VIEW(APP_NAME_KEY),
COOKIES_THEME_KEY: COOKIE_KEY_TEMPLATES.THEME(APP_NAME_KEY),
},

View file

@ -1,3 +1,4 @@
import { ApolloLink, from } from '@apollo/client/core';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
@ -12,25 +13,47 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.callHook('apollo:error', err);
});
const localeCookie = useCookie($appHelpers.COOKIES_LOCALE_KEY);
const accessTokenCookie = useCookie($appHelpers.COOKIES_ACCESS_TOKEN_KEY);
nuxtApp.hook('apollo:error', (error) => {
console.error('[Apollo Error]:', error);
});
const authLink = setContext((_, { headers }) => {
const acceptLanguage = localeCookie.value || 'en-gb';
let accessToken = '';
let locale = 'en-gb';
return {
headers: {
if (import.meta.client) {
const clientCookies = document.cookie.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
acc[key] = decodeURIComponent(value);
return acc;
}, {} as Record<string, string>);
accessToken = clientCookies[$appHelpers.COOKIES_ACCESS_TOKEN_KEY] || '';
locale = clientCookies[$appHelpers.COOKIES_LOCALE_KEY] || 'en-gb';
} else {
const cookieHeader = nuxtApp.ssrContext?.event?.node?.req?.headers?.cookie || '';
const serverCookies = cookieHeader.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
if (key && value) {
acc[key] = decodeURIComponent(value);
}
return acc;
}, {} as Record<string, string>);
accessToken = serverCookies[$appHelpers.COOKIES_ACCESS_TOKEN_KEY] || '';
locale = serverCookies[$appHelpers.COOKIES_LOCALE_KEY] || 'en-gb';
}
const hdrs: Record<string, string> = {
...headers,
'Accept-Language': acceptLanguage,
...(accessTokenCookie.value && {
'X-SCHON-AUTH': `Bearer ${accessTokenCookie.value}`,
}),
},
'Accept-Language': locale
};
if (accessToken) {
hdrs['X-SCHON-AUTH'] = `Bearer ${accessToken}`;
}
return { headers: hdrs };
});
const customLink = new ApolloLink((operation, forward) => {

View file

@ -5,6 +5,7 @@
"createAccount": "Create Account",
"addToCart": "Add to cart",
"removeFromCart": "Remove from cart",
"addAllToCart": "Add all to cart",
"addToWishlist": "Add to Wishlist",
"removeFromWishlist": "Remove from Wishlist",
"send": "Send",
@ -59,7 +60,8 @@
"main": "Error!",
"defaultError": "Something went wrong..",
"noDataToUpdate": "There is no data to update.",
"loginFirst": "You should be logged in to do this action!"
"loginFirst": "You should be logged in to do this action!",
"alreadyInWishlist": "{product} is already in wishlist!"
},
"success": {
"login": "Sign in successes",
@ -75,6 +77,7 @@
"removeFromWishlist": "{product} has been removed from the wishlist!",
"removeAllFromWishlist": "You have successfully emptied the wishlist!",
"bulkRemoveWishlist": "Selected items have been successfully removed from the wishlist!",
"bulkRemoveOrder": "Selected items have been successfully removed from the cart!",
"avatarUpload": "You have successfully uploaded an avatar!",
"userUpdate": "Profile successfully updated!",
"emailUpdate": "Check your inbox for a confirmation link to complete your email update.",
@ -325,8 +328,14 @@
"text": "Discover the latest trends, style inspiration, and fashion insights from our editorial team."
},
"cart": {
"title": "Your Cart",
"items": "no items | {count} item | {count} items"
"title": "My Cart",
"items": "no items | {count} item | {count} items",
"empty": "Your cart is empty."
},
"wishlist": {
"title": "My Wishlist",
"items": "no items | {count} item | {count} items",
"empty": "Your wishlist is empty."
},
"shop": {
"title": "Shop",

View file

@ -1,335 +1 @@
{
"buttons": {
"login": "Войти",
"register": "Регистрация",
"createAccount": "Создать аккаунт",
"addToCart": "В корзину",
"removeFromCart": "Удалить из корзины",
"addToWishlist": "В избранное",
"removeFromWishlist": "Удалить из избранного",
"send": "Отправить",
"goEmail": "Перейти к почте",
"logout": "Выйти",
"checkout": "Оформить заказ",
"save": "Сохранить",
"sendLink": "Отправить ссылку для сброса",
"topUp": "Пополнить",
"shopNow": "КУПИТЬ СЕЙЧАС",
"shopTheSale": "Купить по акции",
"readMore": "Читать далее",
"sendMessage": "Отправить сообщение",
"saveChanges": "Сохранить изменения",
"clearAll": "Очистить всё"
},
"errors": {
"required": "Это поле обязательно!",
"mail": "Введите корректный email!",
"compare": "Пароли не совпадают!",
"needLower": "Добавьте строчную букву.",
"needUpper": "Добавьте заглавную букву.",
"needNumber": "Добавьте цифру.",
"needMin": "Мин. 8 символов",
"needSpecial": "Добавьте спецсимвол: #.?!$%^&*'()_+=:;\"'/>.<,|\\-",
"pageNotFound": "Страница не найдена"
},
"fields": {
"search": "Поиск",
"searchOrder": "Поиск заказа",
"name": "Имя",
"firstName": "Имя",
"lastName": "Фамилия",
"phoneNumber": "Номер телефона",
"email": "Email",
"subject": "Тема",
"message": "Ваше сообщение",
"password": "Пароль",
"newPassword": "Новый пароль",
"confirmPassword": "Подтвердите пароль",
"confirmNewPassword": "Подтвердите новый пароль",
"brandsSearch": "Поиск брендов по названию..."
},
"checkboxes": {
"remember": "Запомнить меня",
"chooseAll": "Выбрать все",
"agree": "Я согласен с {terms} и {policy}",
"subscribe": "Подписаться на рассылку новостей об эксклюзивных предложениях и обновлениях"
},
"popup": {
"errors": {
"main": "Ошибка!",
"defaultError": "Что-то пошло не так...",
"noDataToUpdate": "Нет данных для обновления.",
"loginFirst": "Войдите, чтобы выполнить это действие!"
},
"success": {
"login": "Вход выполнен",
"register": "Аккаунт успешно создан. Пожалуйста, подтвердите ваш Email перед входом!",
"confirmEmail": "Ссылка для подтверждения E-mail успешно отправлена!",
"reset": "Если указанный email существует в нашей системе, мы отправим на него письмо для восстановления пароля!",
"newPassword": "Вы успешно изменили пароль!",
"contactUs": "Ваше сообщение успешно отправлено!",
"addToCart": "{product} добавлен в корзину!",
"removeFromCart": "{product} удален из корзины!",
"removeAllFromCart": "Корзина успешно очищена!",
"addToWishlist": "{product} добавлен в избранное!",
"removeFromWishlist": "{product} удален из избранного!",
"removeAllFromWishlist": "Список избранного успешно очищен!",
"bulkRemoveWishlist": "Выбранные товары удалены из избранного!",
"avatarUpload": "Аватар успешно загружен!",
"userUpdate": "Профиль успешно обновлен!",
"emailUpdate": "Проверьте вашу почту для перехода по ссылке подтверждения и завершения обновления email.",
"referralCopy": "Реферальная ссылка скопирована!",
"promocodeCopy": "Промокод скопирован!",
"configCopy": "Новая ui конфигурация скопирована!",
"addFeedback": "Ваш отзыв сохранен!"
},
"addToCartLimit": "Лимит общего количества составляет {quantity}!",
"failAdd": "Пожалуйста, войдите, чтобы совершить покупку",
"activationSuccess": "E-mail успешно подтвержден. Пожалуйста, войдите!",
"successUpdate": "Профиль успешно обновлен!",
"payment": "Ваш платеж обрабатывается! Пожалуйста, подождите",
"successCheckout": "Заказ успешно оплачен!"
},
"header": {
"nav": {
"shop": "Магазин",
"catalog": "Каталог",
"brands": "Бренды",
"blog": "Блог",
"contact": "Контакты"
},
"actions": {
"wishlist": "Избранное",
"cart": "Корзина",
"login": "Войти",
"profile": "Профиль"
},
"search": {
"empty": "Ничего не найдено"
},
"catalog": {
"title": "Каталог"
}
},
"footer": {
"address": "Адрес: ",
"email": "Email: ",
"phone": "Телефон: ",
"text": "Курируем вневременную роскошную моду для взыскательных личностей",
"shop": "Магазин",
"allProducts": "Все товары",
"catalog": "Каталог",
"brands": "Бренды",
"help": "Помощь",
"rights": "Все права защищены."
},
"home": {
"hero": {
"title": "Вневременная элегантность",
"text": "Откройте для себя нашу кураторскую коллекцию люксовой моды и аксессуаров, определяющих утонченный стиль"
},
"categories": {
"title": "Покупки по категориям"
},
"ad": {
"title": "Весенняя коллекция",
"text1": "Скидки до 40%",
"text2": "Откройте для себя последние тренды в мире люксовой моды"
},
"blog": {
"title": "Из журнала"
}
},
"forms": {
"login": {
"title": "С возвращением",
"subtitle": "Войдите в свой аккаунт, чтобы продолжить",
"forgot": "Забыли пароль?",
"or": "или"
},
"register": {
"title": "Создать аккаунт",
"subtitle": "Зарегистрируйтесь, чтобы начать ваше стильное путешествие",
"login": "Уже есть аккаунт?"
},
"reset": {
"title": "Сброс пароля",
"subtitle": "Введите ваш email, и мы отправим ссылку для сброса пароля.",
"backToLogin": "Назад к входу"
},
"newPassword": {
"title": "Новый пароль"
}
},
"cards": {
"product": {
"stock": "В наличии: "
}
},
"breadcrumbs": {
"home": "Главная",
"catalog": "Каталог",
"contact": "Контакты",
"orders": "Заказы",
"wishlist": "Избранное",
"cart": "Корзина",
"settings": "Настройки",
"balance": "Баланс",
"promocodes": "Промокоды",
"login": "Вход",
"register": "Регистрация",
"resetPassword": "Сброс пароля",
"newPassword": "Новый пароль",
"brands": "Бренды",
"blog": "Блог",
"search": "Поиск",
"categories": "Категории",
"shop": "Магазин",
"policy": "Политика конфиденциальности",
"terms": "Условия использования",
"return": "Политика возврата",
"faq": "Часто задаваемые вопросы",
"shipping": "Информация о доставке",
"about": "О нас"
},
"contact": {
"title": "Свяжитесь с нами",
"text": "Мы будем рады услышать вас. Отправьте нам сообщение, и мы ответим как можно скорее.",
"block": {
"title": "Давайте общаться",
"text": "Есть ли у вас вопросы о наших товарах, нужен совет по стилю или вы хотите сотрудничать с нами — мы здесь, чтобы помочь. Наша команда стремится предоставлять исключительный сервис и поддержку.",
"email": "Написать нам",
"call": "Позвонить нам",
"hours": "Часы работы"
},
"form": {
"title": "Отправить сообщение"
}
},
"store": {
"sorting": "Сортировать по:",
"filters": {
"title": "Фильтры",
"apply": "Применить",
"reset": "Сбросить",
"all": "Все"
}
},
"search": {
"title": "Результаты поиска",
"products": "Товары",
"categories": "Категории",
"brands": "Бренды",
"byRequest": "по запросу"
},
"product": {
"characteristics": "Все характеристики",
"similar": "Вам также может понравиться"
},
"profile": {
"settings": {
"title": "Настройки",
"joinData": "Дата регистрации",
"accountInfo": "Информация об аккаунте",
"copyReferral": "Скопировать реферальную ссылку",
"referralTooltip": "Вы получите реферальную ссылку после успешной покупки"
},
"orders": {
"title": "Заказы",
"chooseStatus": "Выберите статус",
"id": "№",
"price": "Цена",
"total": "Итого",
"empty": "Нет заказов по данным параметрам.",
"statuses": {
"all": "Все",
"failed": "Ошибка",
"payment": "Ожидает оплаты",
"created": "Создан",
"delivering": "Доставляется",
"finished": "Завершен",
"momental": "Моментальный"
},
"searchTooltip": "Введите номер заказа или название товара"
},
"wishlist": {
"title": "Избранное",
"total": "{quantity} товаров на сумму {amount}",
"deleteTooltip": "Удалить всё из избранного",
"empty": "Ваш список избранного пуст."
},
"cart": {
"title": "Корзина",
"quantity": "Количество: ",
"total": "Итого",
"empty": "Ваша корзина пуста."
},
"balance": {
"title": "Баланс"
},
"promocodes": {
"title": "Промокоды",
"until": "До",
"empty": "У вас нет промокодов."
},
"logout": "Выйти"
},
"demo": {
"settings": {
"title": "Демо-настройки",
"ui": "Настройки интерфейса"
},
"buttons": {
"reset": "Сбросить на умолчания",
"save": "Сохранить изменения",
"generateCode": "Сгенерировать код для 'app.config.ts'"
},
"preview": {
"text": "Замените объект UI в 'app.config.ts' на этот код"
},
"descriptions": {
"showBreadcrumbs": "Показывать цепочку навигации на страницах.",
"showSearchBar": "Показывать строку поиска в шапке сайта."
}
},
"docs": {
"faq": {
"title": "Часто задаваемые вопросы"
},
"shipping": {
"title": "Информация о доставке"
},
"return": {
"title": "Политика возврата"
},
"policy": {
"title": "Политика конфиденциальности"
},
"terms": {
"title": "Условия использования"
},
"about": {
"title": "О нас"
}
},
"brands": {
"title": "Люксовые бренды",
"text": "Откройте для себя самые престижные мировые модные дома и люксовые бренды, подобранные для взыскательного вкуса."
},
"catalog": {
"title": "Разнообразные категории",
"text": "Откройте для себя самые престижные мировые модные дома и люксовые категории, подобранные для взыскательного вкуса."
},
"blog": {
"title": "Модный журнал",
"text": "Узнавайте о последних трендах, источниках вдохновения для стиля и модных инсайтах от нашей редакции."
},
"cart": {
"title": "Ваша корзина",
"items": "нет товаров | {count} товар | {count} товара | {count} товаров"
},
"shop": {
"title": "Магазин",
"text": "Откройте для себя нашу кураторскую коллекцию люксовой моды и аксессуаров, определяющих утонченный стиль"
}
}
{}

View file

@ -1 +1,344 @@
{}
{
"buttons": {
"login": "Войти",
"register": "Регистрация",
"createAccount": "Создать аккаунт",
"addToCart": "В корзину",
"removeFromCart": "Удалить из корзины",
"addAllToCart": "Добавить все в корзину",
"addToWishlist": "В избранное",
"removeFromWishlist": "Удалить из избранного",
"send": "Отправить",
"goEmail": "Перейти к почте",
"logout": "Выйти",
"checkout": "Оформить заказ",
"save": "Сохранить",
"sendLink": "Отправить ссылку для сброса",
"topUp": "Пополнить",
"shopNow": "КУПИТЬ СЕЙЧАС",
"shopTheSale": "Купить по акции",
"readMore": "Читать далее",
"sendMessage": "Отправить сообщение",
"saveChanges": "Сохранить изменения",
"clearAll": "Очистить всё"
},
"errors": {
"required": "Это поле обязательно!",
"mail": "Введите корректный email!",
"compare": "Пароли не совпадают!",
"needLower": "Добавьте строчную букву.",
"needUpper": "Добавьте заглавную букву.",
"needNumber": "Добавьте цифру.",
"needMin": "Мин. 8 символов",
"needSpecial": "Добавьте спецсимвол: #.?!$%^&*'()_+=:;\"'/>.<,|\\-",
"pageNotFound": "Страница не найдена"
},
"fields": {
"search": "Поиск",
"searchOrder": "Поиск заказа",
"name": "Имя",
"firstName": "Имя",
"lastName": "Фамилия",
"phoneNumber": "Номер телефона",
"email": "Email",
"subject": "Тема",
"message": "Ваше сообщение",
"password": "Пароль",
"newPassword": "Новый пароль",
"confirmPassword": "Подтвердите пароль",
"confirmNewPassword": "Подтвердите новый пароль",
"brandsSearch": "Поиск брендов по названию..."
},
"checkboxes": {
"remember": "Запомнить меня",
"chooseAll": "Выбрать все",
"agree": "Я согласен с {terms} и {policy}",
"subscribe": "Подписаться на рассылку новостей об эксклюзивных предложениях и обновлениях"
},
"popup": {
"errors": {
"main": "Ошибка!",
"defaultError": "Что-то пошло не так...",
"noDataToUpdate": "Нет данных для обновления.",
"loginFirst": "Войдите, чтобы выполнить это действие!",
"alreadyInWishlist": "{product} уже есть в избранном!"
},
"success": {
"login": "Вход выполнен",
"register": "Аккаунт успешно создан. Пожалуйста, подтвердите ваш Email перед входом!",
"confirmEmail": "Ссылка для подтверждения E-mail успешно отправлена!",
"reset": "Если указанный email существует в нашей системе, мы отправим на него письмо для восстановления пароля!",
"newPassword": "Вы успешно изменили пароль!",
"contactUs": "Ваше сообщение успешно отправлено!",
"addToCart": "{product} добавлен в корзину!",
"removeFromCart": "{product} удален из корзины!",
"removeAllFromCart": "Корзина успешно очищена!",
"addToWishlist": "{product} добавлен в избранное!",
"removeFromWishlist": "{product} удален из избранного!",
"removeAllFromWishlist": "Список избранного успешно очищен!",
"bulkRemoveWishlist": "Выбранные товары удалены из избранного!",
"bulkRemoveOrder": "Выбранные товары удалены из корзины!",
"avatarUpload": "Аватар успешно загружен!",
"userUpdate": "Профиль успешно обновлен!",
"emailUpdate": "Проверьте вашу почту для перехода по ссылке подтверждения и завершения обновления email.",
"referralCopy": "Реферальная ссылка скопирована!",
"promocodeCopy": "Промокод скопирован!",
"configCopy": "Новая ui конфигурация скопирована!",
"addFeedback": "Ваш отзыв сохранен!"
},
"addToCartLimit": "Лимит общего количества составляет {quantity}!",
"failAdd": "Пожалуйста, войдите, чтобы совершить покупку",
"activationSuccess": "E-mail успешно подтвержден. Пожалуйста, войдите!",
"successUpdate": "Профиль успешно обновлен!",
"payment": "Ваш платеж обрабатывается! Пожалуйста, подождите",
"successCheckout": "Заказ успешно оплачен!"
},
"header": {
"nav": {
"shop": "Магазин",
"catalog": "Каталог",
"brands": "Бренды",
"blog": "Блог",
"contact": "Контакты"
},
"actions": {
"wishlist": "Избранное",
"cart": "Корзина",
"login": "Войти",
"profile": "Профиль"
},
"search": {
"empty": "Ничего не найдено"
},
"catalog": {
"title": "Каталог"
}
},
"footer": {
"address": "Адрес: ",
"email": "Email: ",
"phone": "Телефон: ",
"text": "Курируем вневременную роскошную моду для взыскательных личностей",
"shop": "Магазин",
"allProducts": "Все товары",
"catalog": "Каталог",
"brands": "Бренды",
"help": "Помощь",
"rights": "Все права защищены."
},
"home": {
"hero": {
"title": "Вневременная элегантность",
"text": "Откройте для себя нашу кураторскую коллекцию люксовой моды и аксессуаров, определяющих утонченный стиль"
},
"categories": {
"title": "Покупки по категориям"
},
"ad": {
"title": "Весенняя коллекция",
"text1": "Скидки до 40%",
"text2": "Откройте для себя последние тренды в мире люксовой моды"
},
"blog": {
"title": "Из журнала"
}
},
"forms": {
"login": {
"title": "С возвращением",
"subtitle": "Войдите в свой аккаунт, чтобы продолжить",
"forgot": "Забыли пароль?",
"or": "или"
},
"register": {
"title": "Создать аккаунт",
"subtitle": "Зарегистрируйтесь, чтобы начать ваше стильное путешествие",
"login": "Уже есть аккаунт?"
},
"reset": {
"title": "Сброс пароля",
"subtitle": "Введите ваш email, и мы отправим ссылку для сброса пароля.",
"backToLogin": "Назад к входу"
},
"newPassword": {
"title": "Новый пароль"
}
},
"cards": {
"product": {
"stock": "В наличии: "
}
},
"breadcrumbs": {
"home": "Главная",
"catalog": "Каталог",
"contact": "Контакты",
"orders": "Заказы",
"wishlist": "Избранное",
"cart": "Корзина",
"settings": "Настройки",
"balance": "Баланс",
"promocodes": "Промокоды",
"login": "Вход",
"register": "Регистрация",
"resetPassword": "Сброс пароля",
"newPassword": "Новый пароль",
"brands": "Бренды",
"blog": "Блог",
"search": "Поиск",
"categories": "Категории",
"shop": "Магазин",
"policy": "Политика конфиденциальности",
"terms": "Условия использования",
"return": "Политика возврата",
"faq": "Часто задаваемые вопросы",
"shipping": "Информация о доставке",
"about": "О нас"
},
"contact": {
"title": "Свяжитесь с нами",
"text": "Мы будем рады услышать вас. Отправьте нам сообщение, и мы ответим как можно скорее.",
"block": {
"title": "Давайте общаться",
"text": "Есть ли у вас вопросы о наших товарах, нужен совет по стилю или вы хотите сотрудничать с нами — мы здесь, чтобы помочь. Наша команда стремится предоставлять исключительный сервис и поддержку.",
"email": "Написать нам",
"call": "Позвонить нам",
"hours": "Часы работы"
},
"form": {
"title": "Отправить сообщение"
}
},
"store": {
"sorting": "Сортировать по:",
"filters": {
"title": "Фильтры",
"apply": "Применить",
"reset": "Сбросить",
"all": "Все"
}
},
"search": {
"title": "Результаты поиска",
"products": "Товары",
"categories": "Категории",
"brands": "Бренды",
"byRequest": "по запросу"
},
"product": {
"characteristics": "Все характеристики",
"similar": "Вам также может понравиться"
},
"profile": {
"settings": {
"title": "Настройки",
"joinData": "Дата регистрации",
"accountInfo": "Информация об аккаунте",
"copyReferral": "Скопировать реферальную ссылку",
"referralTooltip": "Вы получите реферальную ссылку после успешной покупки"
},
"orders": {
"title": "Заказы",
"chooseStatus": "Выберите статус",
"id": "№",
"price": "Цена",
"total": "Итого",
"empty": "Нет заказов по данным параметрам.",
"statuses": {
"all": "Все",
"failed": "Ошибка",
"payment": "Ожидает оплаты",
"created": "Создан",
"delivering": "Доставляется",
"finished": "Завершен",
"momental": "Моментальный"
},
"searchTooltip": "Введите номер заказа или название товара"
},
"wishlist": {
"title": "Избранное",
"total": "{quantity} товаров на сумму {amount}",
"deleteTooltip": "Удалить всё из избранного",
"empty": "Ваш список избранного пуст."
},
"cart": {
"title": "Корзина",
"quantity": "Количество: ",
"total": "Итого",
"empty": "Ваша корзина пуста."
},
"balance": {
"title": "Баланс"
},
"promocodes": {
"title": "Промокоды",
"until": "До",
"empty": "У вас нет промокодов."
},
"logout": "Выйти"
},
"demo": {
"settings": {
"title": "Демо-настройки",
"ui": "Настройки интерфейса"
},
"buttons": {
"reset": "Сбросить на умолчания",
"save": "Сохранить изменения",
"generateCode": "Сгенерировать код для 'app.config.ts'"
},
"preview": {
"text": "Замените объект UI в 'app.config.ts' на этот код"
},
"descriptions": {
"showBreadcrumbs": "Показывать цепочку навигации на страницах.",
"showSearchBar": "Показывать строку поиска в шапке сайта."
}
},
"docs": {
"faq": {
"title": "Часто задаваемые вопросы"
},
"shipping": {
"title": "Информация о доставке"
},
"return": {
"title": "Политика возврата"
},
"policy": {
"title": "Политика конфиденциальности"
},
"terms": {
"title": "Условия использования"
},
"about": {
"title": "О нас"
}
},
"brands": {
"title": "Люксовые бренды",
"text": "Откройте для себя самые престижные мировые модные дома и люксовые бренды, подобранные для взыскательного вкуса."
},
"catalog": {
"title": "Разнообразные категории",
"text": "Откройте для себя самые престижные мировые модные дома и люксовые категории, подобранные для взыскательного вкуса."
},
"blog": {
"title": "Модный журнал",
"text": "Узнавайте о последних трендах, источниках вдохновения для стиля и модных инсайтах от нашей редакции."
},
"cart": {
"title": "Моя корзина",
"items": "нет товаров | {count} товар | {count} товара | {count} товаров",
"empty": "Ваша корзина пуста"
},
"wishlist": {
"title": "Мои Избранные",
"items": "нет товаров | {count} товар | {count} товара | {count} товаров",
"empty": "Список выших избранных пуст"
},
"shop": {
"title": "Магазин",
"text": "Откройте для себя нашу кураторскую коллекцию люксовой моды и аксессуаров, определяющих утонченный стиль"
}
}