Simplify catalog cards and fix product routes
This commit is contained in:
@@ -1,36 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
import { useQuery } from '@vue/apollo-composable';
|
||||||
import {
|
import {
|
||||||
CatalogProductTypeSettingsDocument,
|
|
||||||
ClientProductsDocument,
|
ClientProductsDocument,
|
||||||
type CatalogProductTypeSettingsQuery,
|
|
||||||
type ClientProductsQuery,
|
type ClientProductsQuery,
|
||||||
} from '~/composables/graphql/generated';
|
} from '~/composables/graphql/generated';
|
||||||
|
|
||||||
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
||||||
type CatalogProductTypeSettingNode = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
|
|
||||||
type ProductTypeCard = {
|
type ProductTypeCard = {
|
||||||
key: string;
|
key: string;
|
||||||
typeLabel: string;
|
typeLabel: string;
|
||||||
productCount: number;
|
|
||||||
widthCount: number;
|
|
||||||
lengthCount: number;
|
|
||||||
customizationDetails: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsQuery = useQuery(ClientProductsDocument);
|
const productsQuery = useQuery(ClientProductsDocument);
|
||||||
const catalogSettingsQuery = useQuery(CatalogProductTypeSettingsDocument);
|
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
|
|
||||||
const loading = computed(() => productsQuery.loading.value || catalogSettingsQuery.loading.value);
|
const loading = computed(() => productsQuery.loading.value);
|
||||||
const error = computed(() => productsQuery.error.value || catalogSettingsQuery.error.value);
|
const error = computed(() => productsQuery.error.value);
|
||||||
|
|
||||||
const catalogSettingsByType = computed<Record<string, CatalogProductTypeSettingNode>>(() => (
|
|
||||||
Object.fromEntries(
|
|
||||||
(catalogSettingsQuery.result.value?.catalogProductTypeSettings ?? [])
|
|
||||||
.map((setting) => [setting.productType, setting]),
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
function normalizeText(value: string | null | undefined) {
|
function normalizeText(value: string | null | undefined) {
|
||||||
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
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)}`;
|
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 productTypeCards = computed<ProductTypeCard[]>(() => {
|
||||||
const products = productsQuery.result.value?.clientProducts ?? [];
|
const products = productsQuery.result.value?.clientProducts ?? [];
|
||||||
const query = search.value.trim().toLowerCase();
|
const query = search.value.trim().toLowerCase();
|
||||||
@@ -123,29 +76,16 @@ const productTypeCards = computed<ProductTypeCard[]>(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [...grouped.entries()]
|
return [...grouped.entries()]
|
||||||
.map(([typeLabel, items]) => {
|
.map(([typeLabel]) => ({
|
||||||
const widthCount = new Set(items.map((item) => item.widthMm).filter((value) => value != null)).size;
|
key: slugifyTypeLabel(typeLabel),
|
||||||
const lengthCount = new Set(items.map((item) => item.lengthM).filter((value) => value != null)).size;
|
typeLabel,
|
||||||
|
}))
|
||||||
return {
|
|
||||||
key: slugifyTypeLabel(typeLabel),
|
|
||||||
typeLabel,
|
|
||||||
productCount: items.length,
|
|
||||||
widthCount,
|
|
||||||
lengthCount,
|
|
||||||
customizationDetails: customizationDetails(typeLabel),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((card) => {
|
.filter((card) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return card.typeLabel.toLowerCase().includes(query);
|
||||||
card.typeLabel,
|
|
||||||
...card.customizationDetails,
|
|
||||||
String(card.productCount),
|
|
||||||
].some((part) => part.toLowerCase().includes(query));
|
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.typeLabel.localeCompare(b.typeLabel, 'ru'));
|
.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-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="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
|
<NuxtLink
|
||||||
v-for="card in productTypeCards"
|
v-for="card in productTypeCards"
|
||||||
:key="card.key"
|
:key="card.key"
|
||||||
:to="`/products/${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
|
<img
|
||||||
:src="createProductCover(card.typeLabel, card.key)"
|
:src="createProductCover(card.typeLabel, card.key)"
|
||||||
:alt="`Превью ${card.typeLabel}`"
|
:alt="`Превью ${card.typeLabel}`"
|
||||||
class="aspect-[4/3] w-full rounded-[24px] object-cover"
|
class="aspect-square w-full rounded-[24px] object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="mt-4 space-y-3">
|
<div class="mt-3">
|
||||||
<div>
|
<h2 class="text-base font-bold leading-5 text-[#163624]">{{ card.typeLabel }}</h2>
|
||||||
<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>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user