Add catalog product images
@@ -6,6 +6,7 @@ import {
|
||||
type CatalogProductTypeSettingsQuery,
|
||||
type ClientProductsQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import { catalogProductImageSrc } from '~/utils/catalogProductImages';
|
||||
import { useClientCart } from '~/composables/useClientCart';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -128,6 +129,10 @@ function createProductCover(name: string, sku: string) {
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function productImageSrc(name: string, sku: string) {
|
||||
return catalogProductImageSrc(name) ?? createProductCover(name, sku);
|
||||
}
|
||||
|
||||
function hydrateProduct(product: ProductNode): ParsedProduct {
|
||||
const normalizedTags = product.tags.map((tag) => normalizeText(tag)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'ru'));
|
||||
|
||||
@@ -633,9 +638,9 @@ function productDetailPath(group: ProductGroup) {
|
||||
class="absolute left-[-212px] top-28 z-10 hidden w-44 rounded-[28px] border border-[#e6efe9] bg-white p-3 shadow-[0_20px_40px_rgba(18,56,36,0.08)] transition hover:-translate-x-2 hover:shadow-[0_28px_48px_rgba(18,56,36,0.12)] 2xl:block"
|
||||
>
|
||||
<img
|
||||
:src="createProductCover(previousGroup.typeLabel, previousGroup.key)"
|
||||
:src="productImageSrc(previousGroup.typeLabel, previousGroup.key)"
|
||||
:alt="`Перейти к товару ${previousGroup.typeLabel}`"
|
||||
class="aspect-square w-full rounded-[20px] object-cover"
|
||||
class="aspect-square w-full rounded-[20px] bg-[#f7fbf8] object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
<p class="mt-3 text-sm font-semibold leading-5 text-[#163624]">{{ previousGroup.typeLabel }}</p>
|
||||
@@ -647,9 +652,9 @@ function productDetailPath(group: ProductGroup) {
|
||||
class="absolute right-[-212px] top-28 z-10 hidden w-44 rounded-[28px] border border-[#e6efe9] bg-white p-3 shadow-[0_20px_40px_rgba(18,56,36,0.08)] transition hover:translate-x-2 hover:shadow-[0_28px_48px_rgba(18,56,36,0.12)] 2xl:block"
|
||||
>
|
||||
<img
|
||||
:src="createProductCover(nextGroup.typeLabel, nextGroup.key)"
|
||||
:src="productImageSrc(nextGroup.typeLabel, nextGroup.key)"
|
||||
:alt="`Перейти к товару ${nextGroup.typeLabel}`"
|
||||
class="aspect-square w-full rounded-[20px] object-cover"
|
||||
class="aspect-square w-full rounded-[20px] bg-[#f7fbf8] object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
<p class="mt-3 text-sm font-semibold leading-5 text-[#163624]">{{ nextGroup.typeLabel }}</p>
|
||||
@@ -676,9 +681,9 @@ function productDetailPath(group: ProductGroup) {
|
||||
class="flex items-center gap-3 rounded-[24px] border border-[#e6efe9] bg-white p-3 shadow-[0_14px_30px_rgba(18,56,36,0.06)]"
|
||||
>
|
||||
<img
|
||||
:src="createProductCover(previousGroup.typeLabel, previousGroup.key)"
|
||||
:src="productImageSrc(previousGroup.typeLabel, previousGroup.key)"
|
||||
:alt="`Перейти к товару ${previousGroup.typeLabel}`"
|
||||
class="h-16 w-16 rounded-2xl object-cover"
|
||||
class="h-16 w-16 rounded-2xl bg-[#f7fbf8] object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
<span class="text-sm font-semibold text-[#163624]">{{ previousGroup.typeLabel }}</span>
|
||||
@@ -690,9 +695,9 @@ function productDetailPath(group: ProductGroup) {
|
||||
class="flex items-center gap-3 rounded-[24px] border border-[#e6efe9] bg-white p-3 shadow-[0_14px_30px_rgba(18,56,36,0.06)]"
|
||||
>
|
||||
<img
|
||||
:src="createProductCover(nextGroup.typeLabel, nextGroup.key)"
|
||||
:src="productImageSrc(nextGroup.typeLabel, nextGroup.key)"
|
||||
:alt="`Перейти к товару ${nextGroup.typeLabel}`"
|
||||
class="h-16 w-16 rounded-2xl object-cover"
|
||||
class="h-16 w-16 rounded-2xl bg-[#f7fbf8] object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
<span class="text-sm font-semibold text-[#163624]">{{ nextGroup.typeLabel }}</span>
|
||||
@@ -703,9 +708,9 @@ function productDetailPath(group: ProductGroup) {
|
||||
<div class="space-y-3">
|
||||
<div class="overflow-hidden rounded-[32px] border border-[#e6efe9] bg-white p-4 shadow-[0_20px_40px_rgba(18,56,36,0.06)]">
|
||||
<img
|
||||
:src="createProductCover(selectedGroup.typeLabel, articleLabel(selectedGroup))"
|
||||
:src="productImageSrc(selectedGroup.typeLabel, articleLabel(selectedGroup))"
|
||||
:alt="selectedGroup.typeLabel"
|
||||
class="aspect-[5/4] w-full rounded-[26px] object-cover"
|
||||
class="aspect-[5/4] w-full rounded-[26px] bg-[#f7fbf8] object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ClientProductsDocument,
|
||||
type ClientProductsQuery,
|
||||
} from '~/composables/graphql/generated';
|
||||
import { catalogProductImageSrc } from '~/utils/catalogProductImages';
|
||||
|
||||
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
||||
type ProductTypeCard = {
|
||||
@@ -60,6 +61,10 @@ function createProductCover(name: string, sku: string) {
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function productImageSrc(name: string, sku: string) {
|
||||
return catalogProductImageSrc(name) ?? createProductCover(name, sku);
|
||||
}
|
||||
|
||||
const productTypeCards = computed<ProductTypeCard[]>(() => {
|
||||
const products = productsQuery.result.value?.clientProducts ?? [];
|
||||
const query = search.value.trim().toLowerCase();
|
||||
@@ -110,9 +115,9 @@ const productTypeCards = computed<ProductTypeCard[]>(() => {
|
||||
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)"
|
||||
:src="productImageSrc(card.typeLabel, card.key)"
|
||||
:alt="`Превью ${card.typeLabel}`"
|
||||
class="aspect-square w-full rounded-[24px] object-cover"
|
||||
class="aspect-square w-full rounded-[24px] bg-[#f7fbf8] object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
|
||||
|
||||
31
app/utils/catalogProductImages.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
const CATALOG_PRODUCT_IMAGE_BY_TYPE: Record<string, string> = {
|
||||
'алюминиевый скотч': '/catalog-products/aluminum-tape.webp',
|
||||
'армированный скотч': '/catalog-products/reinforced-tape.webp',
|
||||
'вспененный скотч': '/catalog-products/double-sided-foam-tape.webp',
|
||||
'двусторонний pvc': '/catalog-products/double-sided-superglue-foam-tape.webp',
|
||||
'двусторонний пп': '/catalog-products/double-sided-polypropylene-tape.webp',
|
||||
'джамбо-рулоны': '/catalog-products/jumbo-rolls.webp',
|
||||
'крепп': '/catalog-products/masking-tape-indoor.webp',
|
||||
'лента алюминиевая': '/catalog-products/aluminum-tape.webp',
|
||||
'лента армированная': '/catalog-products/reinforced-tape.webp',
|
||||
'лента двусторонняя': '/catalog-products/double-sided-polypropylene-tape.webp',
|
||||
'лента малярная': '/catalog-products/masking-tape-indoor.webp',
|
||||
'лента металлизированная': '/catalog-products/metallized-tape.webp',
|
||||
'лента с логотипом': '/catalog-products/logo-tape.webp',
|
||||
'лента сигнальная': '/catalog-products/signal-tape.webp',
|
||||
'лента упаковочная': '/catalog-products/packaging-tape.webp',
|
||||
'малярная лента': '/catalog-products/masking-tape-outdoor.webp',
|
||||
'металлизированный скотч': '/catalog-products/metallized-tape.webp',
|
||||
'перчатки хб': '/catalog-products/cotton-gloves.webp',
|
||||
'сигнальная лента': '/catalog-products/signal-tape.webp',
|
||||
'стретч-пленка': '/catalog-products/stretch-film.webp',
|
||||
'упаковочный скотч': '/catalog-products/packaging-tape.webp',
|
||||
};
|
||||
|
||||
function normalizeCatalogProductType(value: string | null | undefined) {
|
||||
return String(value ?? '').replaceAll(/\s+/g, ' ').trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function catalogProductImageSrc(productType: string | null | undefined) {
|
||||
return CATALOG_PRODUCT_IMAGE_BY_TYPE[normalizeCatalogProductType(productType)] ?? null;
|
||||
}
|
||||
BIN
public/catalog-products/aluminum-tape.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
public/catalog-products/cotton-gloves.webp
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
public/catalog-products/double-sided-fabric-tape.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
public/catalog-products/double-sided-foam-tape.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/catalog-products/double-sided-paper-tape.webp
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
public/catalog-products/double-sided-polypropylene-tape.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/catalog-products/double-sided-superglue-foam-tape.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/catalog-products/jumbo-rolls.webp
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
public/catalog-products/logo-tape.webp
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
public/catalog-products/masking-tape-indoor.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/catalog-products/masking-tape-outdoor.webp
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/catalog-products/metallized-tape.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/catalog-products/packaging-tape.webp
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
public/catalog-products/reinforced-tape.webp
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/catalog-products/signal-tape.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/catalog-products/stretch-film.webp
Normal file
|
After Width: | Height: | Size: 262 KiB |