Files
web-frontend/app/pages/products.vue

172 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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, totalItems, 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="flex flex-wrap items-center justify-between gap-3">
<p class="text-sm text-base-content/75">Добавляй позиции и оформляй заявку в корзине.</p>
<NuxtLink to="/cart" class="btn btn-outline btn-sm">
Корзина: {{ totalItems }}
</NuxtLink>
</div>
</div>
<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 border border-dashed border-base-300 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>