Files
web-frontend/app/pages/products.vue
2026-04-02 18:28:57 +07:00

158 lines
6.2 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 cart = 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]) {
cart.addProduct({
id: product.id,
name: product.name,
sku: product.sku,
isCustomizable: product.isCustomizable,
});
}
</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">
Корзина: {{ cart.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 flex items-center justify-between">
<span class="badge badge-outline">В корзине: {{ cart.getQuantity(product.id) }}</span>
<button
class="btn btn-circle btn-sm border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
@click="addProductToCart(product)"
>
+
</button>
</div>
</div>
</article>
</div>
<div v-else class="alert surface-card border-0">
Ничего не найдено по текущим параметрам.
</div>
</section>
</template>