schon/storefront/components/cards/product.vue
Alexandr SaVBaD Waltz 129ad1a6fa Features: 1) Build standalone pages for search, contact, catalog, category, brand, product, and home with localized metadata and scoped styles; 2) Add extensive TypeScript definitions for API and app-level structures, including products, orders, brands, and categories; 3) Implement i18n configuration with dynamic browser language detection and fallback system;
Fixes: None;

Extra: 1) Create Pinia stores for app, user, category, and company management; 2) Add utility functions for error handling and category slug lookups; 3) Include German locale file and robots.txt for improved SEO and accessibility; 4) Add SVG assets and improve general folder structure for better maintainability.
2025-06-27 00:10:35 +03:00

308 lines
No EOL
6.3 KiB
Vue

<template>
<div
class="card"
:class="{ 'card__list': productView === 'list' }"
>
<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"
>
<SwiperSlide
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"
/>
</SwiperSlide>
</Swiper>
<div class="card__image-placeholder" />
<div
v-for="(_, i) in images"
:key="i"
class="card__block-hover"
:style="{ left: `${(100/ images.length) * i}%`, width: `${100/ images.length}%` }"
@mouseenter="goTo(i)"
@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"
allow-half
disabled
/>
<div class="card__quantity">{{ t('cards.product.stock') }} {{ product.quantity }}</div>
</div>
</div>
<div class="card__bottom">
<ui-button class="card__bottom-button">
{{ t('buttons.addToCart') }}
</ui-button>
<div class="card__bottom-wishlist">
<Icon name="mdi:cards-heart-outline" size="28" />
<!-- <Icon name="mdi:cards-heart" size="28" />-->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type {IProduct} from "~/types/app/products";
import { useAppConfig } from '~/composables/config';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { EffectFade, Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/effect-fade';
import 'swiper/css/pagination'
const props = defineProps<{
product: IProduct;
}>();
const {t} = useI18n();
const { COOKIES_PRODUCT_VIEW_KEY } = useAppConfig()
const productView = useCookie<string>(
COOKIES_PRODUCT_VIEW_KEY as string,
{
default: () => 'grid',
path: '/',
}
)
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 30px 3px 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 {
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>