Show product tags in catalog

This commit is contained in:
Ruslan Bakiev
2026-04-09 14:14:10 +07:00
parent 249e081dec
commit 5173956b06
4 changed files with 61 additions and 8 deletions

View File

@@ -10,11 +10,13 @@ type ParamValue = number | string;
type ParsedProduct = ProductNode & { type ParsedProduct = ProductNode & {
productTypeLabel: string; productTypeLabel: string;
quantityPerBoxOptions: string[]; quantityPerBoxOptions: string[];
normalizedTags: string[];
}; };
type ProductGroup = { type ProductGroup = {
key: string; key: string;
typeLabel: string; typeLabel: string;
tags: string[];
products: ParsedProduct[]; products: ParsedProduct[];
}; };
@@ -89,6 +91,7 @@ function hydrateProduct(product: ProductNode): ParsedProduct {
...product, ...product,
productTypeLabel: normalizeText(product.productType) || 'Без типа', productTypeLabel: normalizeText(product.productType) || 'Без типа',
quantityPerBoxOptions: splitBoxValues(product.quantityPerBox), quantityPerBoxOptions: splitBoxValues(product.quantityPerBox),
normalizedTags: product.tags.map((tag) => normalizeText(tag)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'ru')),
}; };
} }
@@ -140,6 +143,7 @@ const parsedProducts = computed<ParsedProduct[]>(() => {
String(product.thicknessMicron ?? ''), String(product.thicknessMicron ?? ''),
normalizeText(product.sleeveBrand), normalizeText(product.sleeveBrand),
normalizeText(product.quantityPerBox), normalizeText(product.quantityPerBox),
...product.normalizedTags,
].some((part) => part.toLowerCase().includes(query)); ].some((part) => part.toLowerCase().includes(query));
}) })
.sort(compareProducts); .sort(compareProducts);
@@ -149,21 +153,43 @@ const productGroups = computed<ProductGroup[]>(() => {
const map = new Map<string, ParsedProduct[]>(); const map = new Map<string, ParsedProduct[]>();
for (const product of parsedProducts.value) { for (const product of parsedProducts.value) {
const existing = map.get(product.productTypeLabel); const tagsKey = product.normalizedTags.join('|');
const groupKey = `${product.productTypeLabel}::${tagsKey}`;
const existing = map.get(groupKey);
if (existing) { if (existing) {
existing.push(product); existing.push(product);
} else { } else {
map.set(product.productTypeLabel, [product]); map.set(groupKey, [product]);
} }
} }
return [...map.entries()] return [...map.entries()]
.sort((a, b) => a[0].localeCompare(b[0], 'ru')) .sort((a, b) => {
.map(([typeLabel, products]) => ({ const firstProduct = a[1][0];
key: typeLabel.toLowerCase().replaceAll(/\s+/g, '-'), const secondProduct = b[1][0];
typeLabel, const byType = firstProduct.productTypeLabel.localeCompare(secondProduct.productTypeLabel, 'ru');
if (byType !== 0) {
return byType;
}
return firstProduct.normalizedTags.join('|').localeCompare(secondProduct.normalizedTags.join('|'), 'ru');
})
.map(([groupSignature, products]) => {
const firstProduct = products[0];
const key = groupSignature
.toLowerCase()
.replaceAll(/[^a-z0-9а-яё|]+/gi, '-')
.replaceAll('|', '--')
.replaceAll(/-+/g, '-')
.replaceAll(/^-|-$/g, '');
return {
key,
typeLabel: firstProduct.productTypeLabel,
tags: firstProduct.normalizedTags,
products: [...products].sort(compareProducts), products: [...products].sort(compareProducts),
})); };
});
}); });
function sortParamValues(values: ParamValue[]) { function sortParamValues(values: ParamValue[]) {
@@ -461,6 +487,16 @@ function decrementSelected(group: ProductGroup) {
<div class="p-4 md:p-5 xl:col-span-4"> <div class="p-4 md:p-5 xl:col-span-4">
<div class="mb-4"> <div class="mb-4">
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2> <h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
<div v-if="group.tags.length" class="mt-3 flex flex-wrap gap-2">
<span
v-for="tag in group.tags"
:key="`${group.key}-${tag}`"
class="rounded-full bg-[#eef8f1] px-3 py-1 text-xs font-semibold uppercase tracking-[0.06em] text-[#15613d]"
>
{{ tag }}
</span>
</div>
</div> </div>
<div class="grid gap-8 md:grid-cols-2"> <div class="grid gap-8 md:grid-cols-2">
@@ -585,6 +621,7 @@ function decrementSelected(group: ProductGroup) {
<th class="border-b border-base-300">Толщина</th> <th class="border-b border-base-300">Толщина</th>
<th class="border-b border-base-300">Втулка</th> <th class="border-b border-base-300">Втулка</th>
<th class="border-b border-base-300">Короб</th> <th class="border-b border-base-300">Короб</th>
<th class="border-b border-base-300">Теги</th>
<th class="border-b border-base-300 text-right">Действие</th> <th class="border-b border-base-300 text-right">Действие</th>
</tr> </tr>
</thead> </thead>
@@ -596,6 +633,18 @@ function decrementSelected(group: ProductGroup) {
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td> <td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td> <td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
<td class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td> <td class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td>
<td class="border-b border-base-200">
<div v-if="product.normalizedTags.length" class="flex flex-wrap gap-2">
<span
v-for="tag in product.normalizedTags"
:key="`${product.id}-${tag}`"
class="rounded-full bg-[#eef8f1] px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.06em] text-[#15613d]"
>
{{ tag }}
</span>
</div>
<span v-else></span>
</td>
<td class="border-b border-base-200 text-right"> <td class="border-b border-base-200 text-right">
<button <button
v-if="getQuantity(product.id) === 0" v-if="getQuantity(product.id) === 0"

View File

@@ -579,6 +579,7 @@ export type Product = {
quantityPerBox?: Maybe<Scalars['String']['output']>; quantityPerBox?: Maybe<Scalars['String']['output']>;
sku: Scalars['String']['output']; sku: Scalars['String']['output'];
sleeveBrand?: Maybe<Scalars['String']['output']>; sleeveBrand?: Maybe<Scalars['String']['output']>;
tags: Array<Scalars['String']['output']>;
thicknessMicron?: Maybe<Scalars['Int']['output']>; thicknessMicron?: Maybe<Scalars['Int']['output']>;
widthMm?: Maybe<Scalars['Int']['output']>; widthMm?: Maybe<Scalars['Int']['output']>;
}; };
@@ -899,7 +900,7 @@ export type UpdateCartItemQuantityMutation = { __typename?: 'Mutation', updateCa
export type ClientProductsQueryVariables = Exact<{ [key: string]: never; }>; export type ClientProductsQueryVariables = Exact<{ [key: string]: never; }>;
export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, productType?: string | null, widthMm?: number | null, lengthM?: number | null, thicknessMicron?: number | null, sleeveBrand?: string | null, quantityPerBox?: string | null, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> }; export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, productType?: string | null, widthMm?: number | null, lengthM?: number | null, thicknessMicron?: number | null, sleeveBrand?: string | null, quantityPerBox?: string | null, tags: Array<string>, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> };
export type AddBonusTransactionMutationVariables = Exact<{ export type AddBonusTransactionMutationVariables = Exact<{
input: AddBonusTransactionInput; input: AddBonusTransactionInput;
@@ -1629,6 +1630,7 @@ export const ClientProductsDocument = gql`
thicknessMicron thicknessMicron
sleeveBrand sleeveBrand
quantityPerBox quantityPerBox
tags
isCustomizable isCustomizable
availableInWarehouses { availableInWarehouses {
availableQty availableQty

View File

@@ -10,6 +10,7 @@ query ClientProducts {
thicknessMicron thicknessMicron
sleeveBrand sleeveBrand
quantityPerBox quantityPerBox
tags
isCustomizable isCustomizable
availableInWarehouses { availableInWarehouses {
availableQty availableQty

View File

@@ -242,6 +242,7 @@ type Product {
thicknessMicron: Int thicknessMicron: Int
sleeveBrand: String sleeveBrand: String
quantityPerBox: String quantityPerBox: String
tags: [String!]!
isCustomizable: Boolean! isCustomizable: Boolean!
isActive: Boolean! isActive: Boolean!
availableInWarehouses: [ProductWarehouseBalance!]! availableInWarehouses: [ProductWarehouseBalance!]!