schon/storefront/components/cards/product.vue
Alexandr SaVBaD Waltz 40ae24a04c Features: 1) Add SEO-related fragments to GraphQL queries including SEOMETA_FRAGMENT usage in brands, categories, and products queries; 2) Enable localized and dynamic SEO metadata handling in category pages with Vue composables and useSeoMeta; 3) Replace obsolete client-only wrapper with native Nuxt components like nuxt-marquee for enhanced rendering;
Fixes: 1) Correct file path imports by removing `.js` extensions in GraphQL fragments; 2) Resolve typo in `usePromocodeStore` composables to ensure consistent store usage; 3) Add missing `:type="submit"` to login form button for proper form submission handling;

Extra: 1) Remove unused `.idea` and `README.md` files for repository cleanup; 2) Delete extraneous dependencies from `package-lock.json` for streamlined package management; 3) Refactor category slug handling with improved composable logic for cleaner route parameters and SEO alignment.
2025-09-13 12:53:06 +03:00

437 lines
No EOL
9.8 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 }} {{ CURRENCY }}</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">
<div class="card__bottom-inner">
<ui-button
class="card__bottom-button"
v-if="isProductInCart"
@click="overwriteOrder({
type: 'remove',
productUuid: product.uuid,
productName: product.name
})"
:type="'button'"
: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
})"
:type="'button'"
: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 class="tools" v-if="isToolsVisible && 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>
</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";
import {CURRENCY} from "~/config/constants";
const props = defineProps<{
product: IProduct;
isList?: boolean;
isToolsVisible?: 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 productInCartQuantity = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.filter((prod) => prod.node.product.uuid === props.product.uuid)[0].node.quantity;
});
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: stretch;
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: 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 {
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($contrast, 0.5);
border-radius: $default_border_radius;
padding: 5px 10px;
font-size: 14px;
}
&__bottom {
margin-top: auto;
padding: 0 20px 20px 20px;
max-width: 100%;
&-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
}
&-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;
}
}
}
}
.tools {
width: 100%;
border-radius: 4px;
background-color: rgba($accent, 0.2);
display: grid;
grid-template-columns: 1fr 2fr 1fr;
height: 30px;
&__item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
&-count {
border-left: 1px solid $accent;
border-right: 1px solid $accent;
color: $accent;
font-size: 18px;
font-weight: 600;
}
&-button {
cursor: pointer;
background-color: rgba($accent, 0.2);
border-radius: 4px 0 0 4px;
transition: 0.2s;
color: $accent;
font-size: 20px;
font-weight: 500;
@include hover {
background-color: $accent;
color: $white;
}
&:last-child {
border-radius: 0 4px 4px 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($accentDark, 0.3);
border-radius: 0;
opacity: 1;
transition: 0.2s;
}
:deep(.swiper-pagination-line--active) {
background-color: $accentDark;
}
</style>