schon/storefront/components/cards/product.vue
Alexandr SaVBaD Waltz 761fecf67f Features: 1) Add useWishlistOverwrite composable for wishlist mutations, including adding, removing, and bulk actions; 2) Introduce new localized UI texts for cart and wishlist operations; 3) Enhance filtering logic with parseAttributesString and route query synchronization;
Fixes: 1) Replace `ElNotification` calls with `useNotification` utility across all authentication and user-related composables; 2) Add missing semicolons in multiple index exports and styled components; 3) Resolve issues with reactivity in `useStore` composable by renaming and restructuring product variables;

Extra: 1) Refactor localized strings and translations for better readability and maintenance; 2) Tweak styles including scoped styles, z-index adjustments, and SCSS mixins; 3) Remove unused components and imports to streamline storefront layout.
2025-07-06 19:49:26 +03:00

349 lines
No EOL
7.7 KiB
Vue

<template>
<div
class="card"
:class="{ 'card__list': isList }"
>
<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 class="card__content">
<div class="card__price">{{ product.price }}</div>
<p class="card__name">{{ product.name }}</p>
<el-rate
v-model="rating"
size="large"
disabled
/>
<div class="card__quantity">{{ t('cards.product.stock') }} {{ product.quantity }}</div>
</div>
</div>
<div class="card__bottom">
<ui-button
class="card__bottom-button"
v-if="isProductInCart"
@click="overwriteOrder({
type: 'remove',
productUuid: product.uuid,
productName: product.name
})"
:isLoading="removeLoading"
>
{{ t('buttons.removeFromCart') }}
</ui-button>
<ui-button
v-else
class="card__bottom-button"
@click="overwriteOrder({
type: 'add',
productUuid: product.uuid,
productName: product.name
})"
:isLoading="addLoading"
>
{{ t('buttons.addToCart') }}
</ui-button>
<div
class="card__bottom-wishlist"
@click="overwriteWishlist({
type: (isProductInWishlist ? 'remove' : 'add'),
productUuid: product.uuid,
productName: product.name
})"
>
<icon name="mdi:cards-heart" size="28" v-if="isProductInWishlist" />
<icon name="mdi:cards-heart-outline" size="28" v-else />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type {IProduct} from "~/types/app/products";
import { Swiper, SwiperSlide } from 'swiper/vue';
import { EffectFade, Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/effect-fade';
import 'swiper/css/pagination'
import {useWishlistOverwrite} from "~/composables/wishlist";
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
const props = defineProps<{
product: IProduct;
isList?: boolean;
}>();
const {t} = useI18n();
const wishlistStore = useWishlistStore();
const cartStore = useCartStore();
const { overwriteWishlist } = useWishlistOverwrite();
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
const isProductInWishlist = computed(() => {
const el = wishlistStore.wishlist?.products?.edges.find(
(el) => el?.node?.uuid === props.product.uuid
);
return !!el;
});
const isProductInCart = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid);
});
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: 2px solid $accentDark;
width: 100%;
background-color: $white;
transition: 0.2s;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
gap: 20px;
&__list {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
padding: 10px;
& .card__link {
width: fit-content;
padding: 0;
}
& .card__block {
width: 150px;
height: 150px;
}
& .card__bottom {
margin-top: 0;
width: fit-content;
flex-shrink: 0;
padding-inline: 0;
flex-direction: column;
align-items: flex-end;
gap: 10px;
&-button {
width: fit-content;
padding-inline: 25px;
}
}
& .card__wrapper {
flex-direction: row;
}
}
@include hover {
box-shadow: 0 0 20px 2px rgba($accentDark, 0.4);
}
&__wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
}
&__link {
display: block;
width: 100%;
padding: 20px 15px;
}
&__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: $accentLight;
}
}
&__content {
padding-inline: 20px;
}
&__price {
width: fit-content;
background-color: rgba($accent, 0.2);
border-radius: $default_border_radius;
padding: 5px 10px;
margin-bottom: 10px;
font-weight: 700;
font-size: 16px;
}
&__name {
overflow: hidden;
font-weight: 500;
font-size: 14px;
}
&__quantity {
width: fit-content;
background-color: rgba($accent, 0.2);
border-radius: $default_border_radius;
padding: 5px 10px;
font-size: 14px;
}
&__bottom {
margin-top: auto;
padding: 0 20px 20px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
max-width: 100%;
&-button {
width: 84%;
}
&-wishlist {
cursor: pointer;
width: 34px;
height: 34px;
flex-shrink: 0;
background-color: $accent;
border-radius: $default_border_radius;
display: grid;
place-items: center;
transition: 0.2s;
font-size: 22px;
color: $white;
@include hover {
background-color: $accentLight;
}
}
}
}
: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($accentDark, 0.3);
border-radius: 0;
opacity: 1;
transition: 0.2s;
}
:deep(.swiper-pagination-line--active) {
background-color: $accentDark;
}
</style>