Simplify catalog cards and fix product routes
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user