Simplify catalog cards and fix product routes

This commit is contained in:
Ruslan Bakiev
2026-04-09 19:00:25 +07:00
parent 76ab87620e
commit 0236d88b20
2 changed files with 12 additions and 107 deletions

View File

@@ -1,36 +1,21 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import {
CatalogProductTypeSettingsDocument,
ClientProductsDocument,
type CatalogProductTypeSettingsQuery,
type ClientProductsQuery,
} from '~/composables/graphql/generated';
type ProductNode = ClientProductsQuery['clientProducts'][number];
type CatalogProductTypeSettingNode = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
type ProductTypeCard = {
key: string;
typeLabel: string;
productCount: number;
widthCount: number;
lengthCount: number;
customizationDetails: string[];
};
const productsQuery = useQuery(ClientProductsDocument);
const catalogSettingsQuery = useQuery(CatalogProductTypeSettingsDocument);
const search = ref('');
const loading = computed(() => productsQuery.loading.value || catalogSettingsQuery.loading.value);
const error = computed(() => productsQuery.error.value || catalogSettingsQuery.error.value);
const catalogSettingsByType = computed<Record<string, CatalogProductTypeSettingNode>>(() => (
Object.fromEntries(
(catalogSettingsQuery.result.value?.catalogProductTypeSettings ?? [])
.map((setting) => [setting.productType, setting]),
)
));
const loading = computed(() => productsQuery.loading.value);
const error = computed(() => productsQuery.error.value);
function normalizeText(value: string | null | undefined) {
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
@@ -75,38 +60,6 @@ function createProductCover(name: string, sku: string) {
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
function formatLengthRange(setting: CatalogProductTypeSettingNode) {
if (!setting.customLengthMinM || !setting.customLengthMaxM || !setting.customLengthStepM) {
return null;
}
return `${setting.customLengthMinM}-${setting.customLengthMaxM} м`;
}
function customizationDetails(typeLabel: string) {
const setting = catalogSettingsByType.value[typeLabel];
if (!setting) {
return [];
}
const details: string[] = [];
const lengthRange = formatLengthRange(setting);
if (setting.allowCustomLength && lengthRange) {
details.push(`Длина под заказ ${lengthRange}`);
}
if (setting.allowCustomSleeveBrand) {
details.push('Втулка с логотипом');
}
if (setting.allowCustomLabel) {
details.push('Нанесение надписи');
}
return details;
}
const productTypeCards = computed<ProductTypeCard[]>(() => {
const products = productsQuery.result.value?.clientProducts ?? [];
const query = search.value.trim().toLowerCase();
@@ -123,29 +76,16 @@ const productTypeCards = computed<ProductTypeCard[]>(() => {
}
return [...grouped.entries()]
.map(([typeLabel, items]) => {
const widthCount = new Set(items.map((item) => item.widthMm).filter((value) => value != null)).size;
const lengthCount = new Set(items.map((item) => item.lengthM).filter((value) => value != null)).size;
return {
key: slugifyTypeLabel(typeLabel),
typeLabel,
productCount: items.length,
widthCount,
lengthCount,
customizationDetails: customizationDetails(typeLabel),
};
})
.map(([typeLabel]) => ({
key: slugifyTypeLabel(typeLabel),
typeLabel,
}))
.filter((card) => {
if (!query) {
return true;
}
return [
card.typeLabel,
...card.customizationDetails,
String(card.productCount),
].some((part) => part.toLowerCase().includes(query));
return card.typeLabel.toLowerCase().includes(query);
})
.sort((a, b) => a.typeLabel.localeCompare(b.typeLabel, 'ru'));
});
@@ -162,57 +102,22 @@ const productTypeCards = computed<ProductTypeCard[]>(() => {
<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="productTypeCards.length" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-else-if="productTypeCards.length" class="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">
<NuxtLink
v-for="card in productTypeCards"
:key="card.key"
:to="`/products/${card.key}`"
class="surface-card block rounded-3xl p-4 transition hover:-translate-y-0.5 hover:shadow-[0_22px_42px_rgba(18,56,36,0.12)]"
class="surface-card block rounded-3xl p-3 transition hover:-translate-y-0.5 hover:shadow-[0_22px_42px_rgba(18,56,36,0.12)]"
>
<img
:src="createProductCover(card.typeLabel, card.key)"
:alt="`Превью ${card.typeLabel}`"
class="aspect-[4/3] w-full rounded-[24px] object-cover"
class="aspect-square w-full rounded-[24px] object-cover"
loading="lazy"
>
<div class="mt-4 space-y-3">
<div>
<h2 class="text-xl font-bold text-[#163624]">{{ card.typeLabel }}</h2>
<p class="mt-2 text-sm leading-6 text-[#5d7468]">
Откройте карточку товара, чтобы выбрать параметры, посмотреть ограничения и сразу добавить нужный вариант в корзину.
</p>
</div>
<div class="flex flex-wrap gap-2">
<span class="rounded-full bg-[#f2f7f4] px-3 py-1 text-xs font-semibold text-[#355947]">
{{ card.productCount }} вариантов
</span>
<span v-if="card.widthCount" class="rounded-full bg-[#f2f7f4] px-3 py-1 text-xs font-semibold text-[#355947]">
{{ card.widthCount }} ширин
</span>
<span v-if="card.lengthCount" class="rounded-full bg-[#f2f7f4] px-3 py-1 text-xs font-semibold text-[#355947]">
{{ card.lengthCount }} длин
</span>
</div>
<div v-if="card.customizationDetails.length" class="rounded-[20px] bg-[#eef8f1] p-3">
<p class="text-xs font-bold uppercase tracking-[0.12em] text-[#15613d]">Под заказ</p>
<div class="mt-2 flex flex-wrap gap-2">
<span
v-for="detail in card.customizationDetails"
:key="`${card.key}-${detail}`"
class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#15613d]"
>
{{ detail }}
</span>
</div>
</div>
<div class="inline-flex items-center gap-2 text-sm font-semibold text-[#139957]">
<span>Открыть карточку</span>
<span aria-hidden="true"></span>
</div>
<div class="mt-3">
<h2 class="text-base font-bold leading-5 text-[#163624]">{{ card.typeLabel }}</h2>
</div>
</NuxtLink>
</div>