Files
web-frontend/app/pages/products.vue
2026-04-02 17:17:16 +07:00

115 lines
4.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 } from '~/composables/graphql/generated';
const { result, loading, error } = useQuery(ClientProductsDocument);
const search = ref('');
const stockFilter = ref<'ALL' | 'CUSTOM' | 'STANDARD'>('ALL');
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;
});
});
</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
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>
</div>
</article>
</div>
<div v-else class="alert surface-card border-0">
Ничего не найдено по текущим параметрам.
</div>
</section>
</template>