Introduced `useExactProducts` composable to fetch precise product details for guest cart and wishlist items. Improved cookie-based cart and wishlist fallback handling for unauthenticated users. Updated related components and composables for better synchronization and type safety. - Added `useExactProducts` composable leveraging the `GET_EXACT_PRODUCTS` query. - Enhanced `wishlist.vue` and `cart.vue` for reactive updates on guest state changes. - Improved product synchronization logic in `useOrderSync` and `useWishlistSync`. - Updated translations and fixed minor typos in localization files. Improves user experience by ensuring consistent product details, even for guests. No breaking changes.
518 lines
No EOL
12 KiB
Vue
518 lines
No EOL
12 KiB
Vue
<template>
|
|
<div
|
|
class="card"
|
|
:class="{ 'card__list': isList }"
|
|
>
|
|
<div
|
|
class="card__wishlist"
|
|
@click="overwriteWishlist({
|
|
type: (isProductInWishlist ? 'remove' : 'add'),
|
|
productUuid: product.uuid,
|
|
productName: product.name,
|
|
})"
|
|
>
|
|
<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">
|
|
<nuxt-link-locale
|
|
:to="`/product/${product.slug}`"
|
|
class="card__link"
|
|
>
|
|
<div class="card__block">
|
|
<client-only>
|
|
<swiper
|
|
v-if="images.length"
|
|
@swiper="onSwiper"
|
|
:modules="[EffectFade, Pagination]"
|
|
effect="fade"
|
|
:slides-per-view="1"
|
|
:pagination="paginationOptions"
|
|
class="card__swiper"
|
|
>
|
|
<swiper-slide
|
|
v-for="(img, i) in images"
|
|
:key="i"
|
|
class="card__swiper-slide"
|
|
>
|
|
<nuxt-img
|
|
:src="img"
|
|
:alt="product.name"
|
|
loading="lazy"
|
|
class="card__swiper-image"
|
|
format="webp"
|
|
densities="x1"
|
|
/>
|
|
</swiper-slide>
|
|
</swiper>
|
|
<div class="card__image-placeholder" />
|
|
<div
|
|
v-for="(image, idx) in images"
|
|
:key="idx"
|
|
class="card__block-hover"
|
|
:style="{ left: `${(100/ images.length) * idx}%`, width: `${100/ images.length}%` }"
|
|
@mouseenter="goTo(idx)"
|
|
@mouseleave="goTo(0)"
|
|
/>
|
|
</client-only>
|
|
</div>
|
|
</nuxt-link-locale>
|
|
</div>
|
|
<div class="card__content">
|
|
<div class="card__content-inner">
|
|
<div class="card__brand">{{ product.brand.name }}</div>
|
|
<p class="card__name">{{ product.name }}</p>
|
|
<el-rate
|
|
class="card__rating"
|
|
v-model="rating"
|
|
size="large"
|
|
disabled
|
|
/>
|
|
<div class="card__price">{{ product.price }} $</div>
|
|
</div>
|
|
<div class="card__bottom">
|
|
<div class="card__bottom-inner">
|
|
<div class="tools" v-if="isProductInCart">
|
|
<button
|
|
class="tools__item tools__item-button"
|
|
@click="overwriteOrder({
|
|
type: 'remove',
|
|
productUuid: product.uuid,
|
|
productName: product.name
|
|
})"
|
|
>
|
|
-
|
|
</button>
|
|
<span class="tools__item tools__item-count" v-text="'X' + productInCartQuantity" />
|
|
<button
|
|
class="tools__item tools__item-button"
|
|
@click="overwriteOrder({
|
|
type: 'add',
|
|
productUuid: product.uuid,
|
|
productName: product.name
|
|
})"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
<ui-button
|
|
v-else
|
|
class="card__bottom-button"
|
|
@click="overwriteOrder({
|
|
type: 'add',
|
|
productUuid: product.uuid,
|
|
productName: product.name
|
|
})"
|
|
:type="'button'"
|
|
:isLoading="addLoading"
|
|
>
|
|
{{ t('buttons.addToCart') }}
|
|
</ui-button>
|
|
<ui-button
|
|
:type="'button'"
|
|
class="card__bottom-button"
|
|
:style="'secondary'"
|
|
@click="buyProduct(product.uuid)"
|
|
:isLoading="buyLoading"
|
|
>
|
|
{{ t('buttons.buyNow') }}
|
|
</ui-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type {IProduct} from '@types';
|
|
import { Swiper, SwiperSlide } from 'swiper/vue';
|
|
import { EffectFade, Pagination } from 'swiper/modules';
|
|
import {useWishlistOverwrite} from '@composables/wishlist';
|
|
import {useOrderOverwrite} from '@composables/orders';
|
|
import {useProductBuy} from "@composables/products";
|
|
|
|
const props = defineProps<{
|
|
product: IProduct;
|
|
isList?: boolean;
|
|
}>();
|
|
|
|
const {t} = useI18n();
|
|
const wishlistStore = useWishlistStore();
|
|
const cartStore = useCartStore();
|
|
const userStore = useUserStore();
|
|
const { $appHelpers } = useNuxtApp();
|
|
|
|
const { overwriteWishlist } = useWishlistOverwrite();
|
|
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
|
|
const { buyProduct, loading: buyLoading } = useProductBuy();
|
|
|
|
const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
|
|
default: () => [],
|
|
path: '/',
|
|
});
|
|
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
|
default: () => [],
|
|
path: '/',
|
|
});
|
|
|
|
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
|
|
|
const isProductInWishlist = computed(() => {
|
|
if (isAuthenticated.value) {
|
|
return !!wishlistStore.wishlist?.products?.edges.find(
|
|
(el) => el?.node?.uuid === props.product.uuid
|
|
);
|
|
} else {
|
|
return (cookieWishlist.value ?? []).includes(props.product.uuid);
|
|
}
|
|
});
|
|
const isProductInCart = computed(() => {
|
|
if (isAuthenticated.value) {
|
|
return !!cartStore.currentOrder?.orderProducts?.edges.find(
|
|
(prod) => prod.node.product.uuid === props.product?.uuid
|
|
);
|
|
} else {
|
|
return (cookieCart.value ?? []).some(
|
|
(item) => item.productUuid === props.product?.uuid
|
|
);
|
|
}
|
|
});
|
|
|
|
const productInCartQuantity = computed(() => {
|
|
if (isAuthenticated.value) {
|
|
const productEdge = cartStore.currentOrder?.orderProducts?.edges.find(
|
|
(prod) => prod.node.product.uuid === props.product.uuid
|
|
);
|
|
return productEdge?.node.quantity ?? 0;
|
|
} else {
|
|
const cartItem = (cookieCart.value ?? []).find(
|
|
(item) => item.productUuid === props.product.uuid
|
|
);
|
|
return cartItem?.quantity ?? 0;
|
|
}
|
|
});
|
|
|
|
const rating = computed(() => {
|
|
return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
|
|
});
|
|
|
|
const images = computed(() =>
|
|
props.product.images.edges.map(e => e.node.image)
|
|
);
|
|
const paginationOptions = computed(() =>
|
|
images.value.length > 1
|
|
? {
|
|
clickable: true,
|
|
bulletClass: 'swiper-pagination-line',
|
|
bulletActiveClass: 'swiper-pagination-line--active'
|
|
}
|
|
: false
|
|
);
|
|
|
|
const swiperRef = ref<any>(null);
|
|
|
|
function onSwiper(swiper: any) {
|
|
swiperRef.value = swiper;
|
|
}
|
|
|
|
function goTo(index: number) {
|
|
swiperRef.value?.slideTo(index);
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.card {
|
|
border-radius: $default_border_radius;
|
|
border: 1px solid $border;
|
|
width: 100%;
|
|
background-color: $main;
|
|
position: relative;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
&__list {
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
gap: 50px;
|
|
padding: 15px;
|
|
|
|
& .card__link {
|
|
width: fit-content;
|
|
padding: 0;
|
|
}
|
|
|
|
& .card__block {
|
|
width: 150px;
|
|
height: 150px;
|
|
}
|
|
|
|
& .card__content {
|
|
width: 100%;
|
|
flex-direction: row;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
padding: 0;
|
|
|
|
&-inner {
|
|
align-self: flex-start;
|
|
}
|
|
}
|
|
|
|
& .tools {
|
|
width: 136px;
|
|
}
|
|
|
|
& .card__bottom {
|
|
margin-top: 0;
|
|
width: fit-content;
|
|
flex-shrink: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
|
|
&-inner {
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 10px;
|
|
}
|
|
|
|
&-button {
|
|
width: fit-content;
|
|
padding-inline: 25px;
|
|
}
|
|
}
|
|
|
|
& .card__wrapper {
|
|
flex-direction: row;
|
|
}
|
|
}
|
|
|
|
@include hover {
|
|
border-color: $border_hover;
|
|
box-shadow: 0 0 10px 1px $border;
|
|
}
|
|
|
|
&__wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
&__link {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
&__block {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 200px;
|
|
overflow: hidden;
|
|
|
|
&-hover {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 20%;
|
|
height: 100%;
|
|
z-index: 2;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
}
|
|
}
|
|
|
|
&__swiper {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
z-index: 1;
|
|
padding-bottom: 10px;
|
|
|
|
&-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
}
|
|
|
|
&__image {
|
|
&-placeholder {
|
|
width: 100%;
|
|
height: 200px;
|
|
background-color: $primary;
|
|
}
|
|
}
|
|
|
|
&__content {
|
|
padding: 10px 20px 20px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
|
|
&-inner {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
}
|
|
|
|
&__brand {
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
letter-spacing: -0.2px;
|
|
color: $text;
|
|
}
|
|
|
|
&__price {
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
letter-spacing: -0.5px;
|
|
color: $primary_dark;
|
|
}
|
|
|
|
&__rating {
|
|
margin-block: 4px 10px;
|
|
}
|
|
|
|
&__name {
|
|
word-break: break-all;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 1;
|
|
overflow: hidden;
|
|
color: $primary_dark;
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
&__quantity {
|
|
width: fit-content;
|
|
background-color: rgba($secondary, 0.5);
|
|
border-radius: $less_border_radius;
|
|
padding: 5px 10px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
&__wishlist {
|
|
cursor: pointer;
|
|
width: fit-content;
|
|
background-color: $main;
|
|
box-shadow: 0 2px 4px 0 $primary_shadow;
|
|
position: absolute;
|
|
top: 16px;
|
|
right: 16px;
|
|
z-index: 3;
|
|
border-radius: 50%;
|
|
padding: 12px;
|
|
|
|
@include hover {
|
|
box-shadow: 0 2px 4px 0 $primary_shadow_hover;
|
|
}
|
|
|
|
& span {
|
|
color: $secondary;
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
&__bottom {
|
|
&-inner {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
&-button {
|
|
padding-block: 10px !important;
|
|
border-radius: $less_border_radius;
|
|
}
|
|
|
|
&-wishlist {
|
|
cursor: pointer;
|
|
width: 34px;
|
|
height: 34px;
|
|
flex-shrink: 0;
|
|
background-color: $primary;
|
|
border-radius: $less_border_radius;
|
|
display: grid;
|
|
place-items: center;
|
|
|
|
font-size: 22px;
|
|
color: $main;
|
|
|
|
@include hover {
|
|
background-color: $primary_hover;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.tools {
|
|
width: 100%;
|
|
border-radius: $less_border_radius;
|
|
background-color: $primary;
|
|
display: grid;
|
|
grid-template-columns: 1fr 2fr 1fr;
|
|
height: 40px;
|
|
|
|
&__item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
&-count {
|
|
border-left: 1px solid $primary_hover;
|
|
border-right: 1px solid $primary_hover;
|
|
|
|
color: $main;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
&-button {
|
|
cursor: pointer;
|
|
background-color: $primary;
|
|
border-radius: $less_border_radius 0 0 $less_border_radius;
|
|
|
|
color: $main;
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
|
|
@include hover {
|
|
background-color: $primary_hover;
|
|
color: $main;
|
|
}
|
|
|
|
&:last-child {
|
|
border-radius: 0 $less_border_radius $less_border_radius 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
:deep(.swiper-pagination) {
|
|
bottom: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
:deep(.swiper-pagination-line) {
|
|
display: inline-block;
|
|
width: 24px;
|
|
height: 2px;
|
|
background-color: rgba($primary_dark, 0.3);
|
|
border-radius: 0;
|
|
opacity: 1;
|
|
}
|
|
|
|
:deep(.swiper-pagination-line--active) {
|
|
background-color: $primary_dark;
|
|
}
|
|
</style> |