163 lines
6.3 KiB
Vue
163 lines
6.3 KiB
Vue
<script setup lang="ts">
|
||
import { useQuery } from '@vue/apollo-composable';
|
||
import { ClientProductsDocument, type ClientProductsQuery } from '~/composables/graphql/generated';
|
||
import { useClientCart } from '~/composables/useClientCart';
|
||
|
||
const { result, loading, error } = useQuery(ClientProductsDocument);
|
||
const search = ref('');
|
||
const stockFilter = ref<'ALL' | 'CUSTOM' | 'STANDARD'>('ALL');
|
||
const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart();
|
||
|
||
const coverPresets = [
|
||
['#e9fbe5', '#acfcd5', '#7be9aa'],
|
||
['#f5fff7', '#d9f5e6', '#8bd8b0'],
|
||
['#fef4ed', '#ffe5d8', '#ffd1b8'],
|
||
];
|
||
|
||
function createProductCover(name: string, sku: string) {
|
||
const seed = `${name}${sku}`.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||
const [start, middle, finish] = coverPresets[seed % coverPresets.length];
|
||
const firstLetter = name.trim().charAt(0).toUpperCase() || 'P';
|
||
|
||
const svg = `
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
|
||
<defs>
|
||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0%" stop-color="${start}" />
|
||
<stop offset="56%" stop-color="${middle}" />
|
||
<stop offset="100%" stop-color="${finish}" />
|
||
</linearGradient>
|
||
</defs>
|
||
<rect width="640" height="480" fill="url(#g)" rx="38" />
|
||
<g opacity="0.18">
|
||
<circle cx="520" cy="66" r="100" fill="#0d854a" />
|
||
<circle cx="80" cy="440" r="100" fill="#0d854a" />
|
||
</g>
|
||
<text x="50%" y="53%" text-anchor="middle" fill="#0f2f20" font-family="Manrope, sans-serif" font-size="186" font-weight="700">${firstLetter}</text>
|
||
</svg>
|
||
`.trim();
|
||
|
||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||
}
|
||
|
||
const filteredProducts = computed(() => {
|
||
const list = result.value?.clientProducts ?? [];
|
||
const normalizedSearch = search.value.trim().toLowerCase();
|
||
|
||
return list.filter((product) => {
|
||
const matchSearch = !normalizedSearch
|
||
|| product.name.toLowerCase().includes(normalizedSearch)
|
||
|| product.sku.toLowerCase().includes(normalizedSearch);
|
||
|
||
const matchType = stockFilter.value === 'ALL'
|
||
|| (stockFilter.value === 'CUSTOM' && product.isCustomizable)
|
||
|| (stockFilter.value === 'STANDARD' && !product.isCustomizable);
|
||
|
||
return matchSearch && matchType;
|
||
});
|
||
});
|
||
|
||
function addProductToCart(product: ClientProductsQuery['clientProducts'][number]) {
|
||
addProduct({
|
||
id: product.id,
|
||
name: product.name,
|
||
sku: product.sku,
|
||
isCustomizable: product.isCustomizable,
|
||
});
|
||
}
|
||
|
||
function incrementProduct(productId: string) {
|
||
incrementQuantity(productId);
|
||
}
|
||
|
||
function decrementProduct(productId: string) {
|
||
decrementQuantity(productId);
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="space-y-5">
|
||
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
|
||
|
||
<div class="surface-card rounded-3xl p-4 md:p-5">
|
||
<div class="grid gap-3 md:grid-cols-[1fr_auto]">
|
||
<label class="form-control">
|
||
<span class="label-text">Поиск</span>
|
||
<input
|
||
v-model="search"
|
||
type="text"
|
||
class="input input-bordered w-full"
|
||
placeholder="Название или SKU"
|
||
>
|
||
</label>
|
||
|
||
<label class="form-control md:min-w-56">
|
||
<span class="label-text">Фильтр</span>
|
||
<select v-model="stockFilter" class="select select-bordered w-full">
|
||
<option value="ALL">Все товары</option>
|
||
<option value="CUSTOM">Только кастомные</option>
|
||
<option value="STANDARD">Только стандартные</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="loading" class="alert surface-card border-0">Загрузка каталога...</div>
|
||
<div v-else-if="error" class="alert alert-error">{{ error.message }}</div>
|
||
|
||
<div v-else-if="filteredProducts.length > 0" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||
<article class="surface-card overflow-hidden rounded-3xl p-4">
|
||
<div class="flex h-full flex-col justify-between gap-4">
|
||
<div>
|
||
<div class="badge badge-outline">Кастом</div>
|
||
<h2 class="mt-3 text-lg font-bold text-[#133826]">Конструктор скотча</h2>
|
||
<p class="mt-1 text-sm text-base-content/75">
|
||
Отдельная карточка под индивидуальную конфигурацию. Параметры добавим следующим шагом.
|
||
</p>
|
||
</div>
|
||
<button class="btn btn-disabled w-full">Скоро</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article
|
||
v-for="(product, index) in filteredProducts"
|
||
:key="product.id"
|
||
class="surface-card product-card-anim overflow-hidden rounded-3xl p-3"
|
||
:style="{ animationDelay: `${index * 55}ms` }"
|
||
>
|
||
<figure class="overflow-hidden rounded-2xl">
|
||
<img
|
||
:src="createProductCover(product.name, product.sku)"
|
||
:alt="`Изображение товара ${product.name}`"
|
||
class="h-48 w-full object-cover transition duration-300 hover:scale-105"
|
||
loading="lazy"
|
||
>
|
||
</figure>
|
||
<div class="px-1 pb-2 pt-3">
|
||
<h2 class="text-lg font-bold text-[#133826]">{{ product.name }}</h2>
|
||
<p class="text-xs text-base-content/65">SKU: {{ product.sku }}</p>
|
||
<div class="mt-3">
|
||
<button
|
||
v-if="getQuantity(product.id) === 0"
|
||
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
|
||
@click="addProductToCart(product)"
|
||
>
|
||
В корзину
|
||
</button>
|
||
|
||
<div v-else class="flex items-center justify-between rounded-2xl border border-base-300 bg-base-100 px-2 py-1">
|
||
<button class="btn btn-square btn-sm" @click="decrementProduct(product.id)">-</button>
|
||
<span class="min-w-8 text-center font-semibold">{{ getQuantity(product.id) }}</span>
|
||
<button class="btn btn-square btn-sm" @click="incrementProduct(product.id)">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<div v-else class="alert surface-card border-0">
|
||
Ничего не найдено по текущим параметрам.
|
||
</div>
|
||
</section>
|
||
</template>
|