Polish header pill sizing and simplify catalog/cart layouts

This commit is contained in:
Ruslan Bakiev
2026-04-03 09:48:46 +07:00
parent 5597c468a2
commit 0aad9177f8
5 changed files with 228 additions and 92 deletions

View File

@@ -188,6 +188,14 @@ body {
min-height: 2rem; min-height: 2rem;
} }
.lk-nav-link-brand {
justify-content: center;
min-width: 5.5rem;
font-size: 0.98rem;
font-weight: 800;
letter-spacing: 0.03em;
}
.lk-pill-divider { .lk-pill-divider {
background: color-mix(in oklab, var(--color-base-content) 20%, transparent); background: color-mix(in oklab, var(--color-base-content) 20%, transparent);
opacity: 0.45; opacity: 0.45;

View File

@@ -33,9 +33,9 @@ function isActive(path: string) {
<div class="fregat-header-row relative flex items-center"> <div class="fregat-header-row relative flex items-center">
<NuxtLink <NuxtLink
to="/" to="/"
class="fregat-pill-glass fregat-pill-shell fregat-pill-brand inline-flex items-center justify-center rounded-full px-5 text-base font-extrabold tracking-[0.03em] text-base-content" class="fregat-pill-glass fregat-pill-shell fregat-pill-brand inline-flex items-center rounded-full p-1"
> >
Fregat <span class="lk-nav-link lk-nav-link-brand">Fregat</span>
</NuxtLink> </NuxtLink>
<nav class="fregat-pill-glass fregat-pill-shell fregat-pill-center flex min-w-0 flex-1 items-center rounded-full p-1 lg:absolute lg:left-1/2 lg:w-auto lg:-translate-x-1/2"> <nav class="fregat-pill-glass fregat-pill-shell fregat-pill-center flex min-w-0 flex-1 items-center rounded-full p-1 lg:absolute lg:left-1/2 lg:w-auto lg:-translate-x-1/2">

View File

@@ -97,76 +97,71 @@ async function submitCart() {
<section class="space-y-6"> <section class="space-y-6">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина</h1> <h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина</h1>
<div class="surface-card space-y-4 rounded-3xl p-5 md:p-6"> <div v-if="counterpartyLoading.value" class="alert surface-card border-0">
<div v-if="counterpartyLoading.value" class="alert"> Проверяем карточку контрагента...
Проверяем карточку контрагента... </div>
</div> <div v-else-if="!isCounterpartyComplete" class="alert alert-warning">
<div v-else-if="!isCounterpartyComplete" class="alert alert-warning"> Для оформления заявки заполните карточку контрагента в
Для оформления заявки заполните карточку контрагента в <NuxtLink to="/profile" class="link link-hover font-semibold">профиле</NuxtLink>.
<NuxtLink to="/profile" class="link link-hover font-semibold">профиле</NuxtLink>. </div>
</div>
<div class="rounded-2xl border border-base-300 bg-base-100 p-4"> <h2 class="text-xl font-bold text-[#123824]">Позиции</h2>
<h2 class="text-lg font-bold text-[#123824]">Список позиций</h2>
<div v-if="cartItems.length === 0" class="alert mt-3"> <div v-if="cartItems.length === 0" class="alert surface-card border-0">
Корзина пока пустая. Добавьте товар из каталога. Корзина пока пустая. Добавьте товар из каталога.
</div>
<ul v-else class="space-y-3">
<li
v-for="item in cartItems"
:key="item.productId"
class="surface-card flex flex-col gap-3 rounded-3xl px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5 md:py-5"
>
<div>
<p class="font-semibold text-[#123824]">{{ item.productName }}</p>
<p class="text-xs opacity-70">SKU: {{ item.sku }}</p>
<p class="text-sm opacity-80">
Объем: {{ lineVolume(item.productId) }}
</p>
</div> </div>
<ul v-else class="mt-3 space-y-2"> <div class="flex items-center gap-2">
<li <button class="btn btn-square btn-sm" @click="decrement(item.productId)">-</button>
v-for="item in cartItems" <span class="min-w-8 text-center font-semibold">{{ item.quantity }}</span>
:key="item.productId" <button class="btn btn-square btn-sm" @click="increment(item.productId)">+</button>
class="flex flex-col gap-3 rounded-xl border border-[#d6ebde] bg-white/75 px-3 py-3 md:flex-row md:items-center md:justify-between" <button class="btn btn-ghost btn-sm text-error" @click="removeFromCart(item.productId)">
> Удалить
<div> </button>
<p class="font-semibold text-[#123824]">{{ item.productName }}</p> </div>
<p class="text-xs opacity-70">SKU: {{ item.sku }}</p> </li>
<p class="text-sm opacity-80"> </ul>
Объем: {{ lineVolume(item.productId) }}
</p>
</div>
<div class="flex items-center gap-2"> <div class="divider my-1" />
<button class="btn btn-square btn-sm" @click="decrement(item.productId)">-</button>
<span class="min-w-8 text-center font-semibold">{{ item.quantity }}</span> <div class="space-y-2 text-sm text-[#214735]">
<button class="btn btn-square btn-sm" @click="increment(item.productId)">+</button> <div class="flex items-center justify-between">
<button class="btn btn-ghost btn-sm text-error" @click="removeFromCart(item.productId)"> <span>Позиций</span>
Удалить <span class="font-semibold">{{ totalPositions }}</span>
</button>
</div>
</li>
</ul>
</div> </div>
<div class="flex items-center justify-between">
<div class="rounded-2xl border border-base-300 bg-base-100 p-4"> <span>Количество, шт.</span>
<h2 class="text-lg font-bold text-[#123824]">Итого</h2> <span class="font-semibold">{{ totalItems }}</span>
<ul class="mt-3 space-y-2 text-sm text-[#214735]"> </div>
<li class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span>Позиций</span> <span>Суммарный объем</span>
<span class="font-semibold">{{ totalPositions }}</span> <span class="font-semibold">{{ totalVolume }}</span>
</li>
<li class="flex items-center justify-between">
<span>Количество, шт.</span>
<span class="font-semibold">{{ totalItems }}</span>
</li>
<li class="flex items-center justify-between">
<span>Суммарный объем</span>
<span class="font-semibold">{{ totalVolume }}</span>
</li>
</ul>
</div> </div>
<button
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sending || counterpartyLoading.value || !isCounterpartyComplete || cartItems.length === 0"
@click="submitCart"
>
{{ sending ? 'Отправляем…' : 'Оформить заявку' }}
</button>
<div v-if="success" class="alert alert-success">{{ success }}</div>
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
</div> </div>
<button
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sending || counterpartyLoading.value || !isCounterpartyComplete || cartItems.length === 0"
@click="submitCart"
>
{{ sending ? 'Отправляем…' : 'Оформить заявку' }}
</button>
<div v-if="success" class="alert alert-success">{{ success }}</div>
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
</section> </section>
</template> </template>

View File

@@ -1,24 +1,162 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import { ClientProductsDocument, type ClientProductsQuery } from '~/composables/graphql/generated';
import { useClientCart } from '~/composables/useClientCart';
const { result, loading, error } = useQuery(ClientProductsDocument);
const search = ref('');
const stockFilter = ref<'ALL' | 'CUSTOM' | 'STANDARD'>('ALL');
const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart();
const coverPresets = [
['#e9fbe5', '#acfcd5', '#7be9aa'],
['#f5fff7', '#d9f5e6', '#8bd8b0'],
['#fef4ed', '#ffe5d8', '#ffd1b8'],
];
function createProductCover(name: string, sku: string) {
const seed = `${name}${sku}`.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const [start, middle, finish] = coverPresets[seed % coverPresets.length];
const firstLetter = name.trim().charAt(0).toUpperCase() || 'P';
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${start}" />
<stop offset="56%" stop-color="${middle}" />
<stop offset="100%" stop-color="${finish}" />
</linearGradient>
</defs>
<rect width="640" height="480" fill="url(#g)" rx="38" />
<g opacity="0.18">
<circle cx="520" cy="66" r="100" fill="#0d854a" />
<circle cx="80" cy="440" r="100" fill="#0d854a" />
</g>
<text x="50%" y="53%" text-anchor="middle" fill="#0f2f20" font-family="Manrope, sans-serif" font-size="186" font-weight="700">${firstLetter}</text>
</svg>
`.trim();
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
}
const filteredProducts = computed(() => {
const list = result.value?.clientProducts ?? [];
const normalizedSearch = search.value.trim().toLowerCase();
return list.filter((product) => {
const matchSearch = !normalizedSearch
|| product.name.toLowerCase().includes(normalizedSearch)
|| product.sku.toLowerCase().includes(normalizedSearch);
const matchType = stockFilter.value === 'ALL'
|| (stockFilter.value === 'CUSTOM' && product.isCustomizable)
|| (stockFilter.value === 'STANDARD' && !product.isCustomizable);
return matchSearch && matchType;
});
});
function addProductToCart(product: ClientProductsQuery['clientProducts'][number]) {
addProduct({
id: product.id,
name: product.name,
sku: product.sku,
isCustomizable: product.isCustomizable,
});
}
function incrementProduct(productId: string) {
incrementQuantity(productId);
}
function decrementProduct(productId: string) {
decrementQuantity(productId);
}
</script>
<template> <template>
<section class="space-y-6"> <section class="space-y-5">
<div class="surface-card rounded-[30px] p-6 md:p-8"> <h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
<h1 class="text-3xl font-extrabold text-[#0f2f20] md:text-4xl">Личный кабинет клиента</h1>
<div class="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-5"> <div class="surface-card rounded-3xl p-4 md:p-5">
<NuxtLink to="/products" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md"> <div class="grid gap-3 md:grid-cols-[1fr_auto]">
<h2 class="font-bold text-[#123824]">Каталог</h2> <label class="form-control">
</NuxtLink> <span class="label-text">Поиск</span>
<NuxtLink to="/cart" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md"> <input
<h2 class="font-bold text-[#123824]">Корзина</h2> v-model="search"
</NuxtLink> type="text"
<NuxtLink to="/orders" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md"> class="input input-bordered w-full"
<h2 class="font-bold text-[#123824]">Мои заказы</h2> placeholder="Название или SKU"
</NuxtLink> >
<NuxtLink to="/profile" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md"> </label>
<h2 class="font-bold text-[#123824]">Профиль</h2>
</NuxtLink> <label class="form-control md:min-w-56">
<NuxtLink to="/notifications" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md"> <span class="label-text">Фильтр</span>
<h2 class="font-bold text-[#123824]">Уведомления</h2> <select v-model="stockFilter" class="select select-bordered w-full">
</NuxtLink> <option value="ALL">Все товары</option>
<option value="CUSTOM">Только кастомные</option>
<option value="STANDARD">Только стандартные</option>
</select>
</label>
</div> </div>
</div> </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="filteredProducts.length > 0" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<article class="surface-card overflow-hidden rounded-3xl p-4">
<div class="flex h-full flex-col justify-between gap-4">
<div>
<div class="badge badge-outline">Кастом</div>
<h2 class="mt-3 text-lg font-bold text-[#133826]">Конструктор скотча</h2>
<p class="mt-1 text-sm text-base-content/75">
Отдельная карточка под индивидуальную конфигурацию. Параметры добавим следующим шагом.
</p>
</div>
<button class="btn btn-disabled w-full">Скоро</button>
</div>
</article>
<article
v-for="(product, index) in filteredProducts"
:key="product.id"
class="surface-card product-card-anim overflow-hidden rounded-3xl p-3"
:style="{ animationDelay: `${index * 55}ms` }"
>
<figure class="overflow-hidden rounded-2xl">
<img
:src="createProductCover(product.name, product.sku)"
:alt="`Изображение товара ${product.name}`"
class="h-48 w-full object-cover transition duration-300 hover:scale-105"
loading="lazy"
>
</figure>
<div class="px-1 pb-2 pt-3">
<h2 class="text-lg font-bold text-[#133826]">{{ product.name }}</h2>
<p class="text-xs text-base-content/65">SKU: {{ product.sku }}</p>
<div class="mt-3">
<button
v-if="getQuantity(product.id) === 0"
class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
@click="addProductToCart(product)"
>
В корзину
</button>
<div v-else class="flex items-center justify-between rounded-2xl border border-base-300 bg-base-100 px-2 py-1">
<button class="btn btn-square btn-sm" @click="decrementProduct(product.id)">-</button>
<span class="min-w-8 text-center font-semibold">{{ getQuantity(product.id) }}</span>
<button class="btn btn-square btn-sm" @click="incrementProduct(product.id)">+</button>
</div>
</div>
</div>
</article>
</div>
<div v-else class="alert surface-card border-0">
Ничего не найдено по текущим параметрам.
</div>
</section> </section>
</template> </template>

View File

@@ -6,7 +6,7 @@ import { useClientCart } from '~/composables/useClientCart';
const { result, loading, error } = useQuery(ClientProductsDocument); const { result, loading, error } = useQuery(ClientProductsDocument);
const search = ref(''); const search = ref('');
const stockFilter = ref<'ALL' | 'CUSTOM' | 'STANDARD'>('ALL'); const stockFilter = ref<'ALL' | 'CUSTOM' | 'STANDARD'>('ALL');
const { addProduct, getQuantity, totalItems, incrementQuantity, decrementQuantity } = useClientCart(); const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart();
const coverPresets = [ const coverPresets = [
['#e9fbe5', '#acfcd5', '#7be9aa'], ['#e9fbe5', '#acfcd5', '#7be9aa'],
@@ -77,12 +77,7 @@ function decrementProduct(productId: string) {
<template> <template>
<section class="space-y-5"> <section class="space-y-5">
<div class="flex flex-wrap items-center justify-between gap-3"> <h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
<NuxtLink to="/cart" class="btn btn-outline btn-sm">
Корзина: {{ totalItems }}
</NuxtLink>
</div>
<div class="surface-card rounded-3xl p-4 md:p-5"> <div class="surface-card rounded-3xl p-4 md:p-5">
<div class="grid gap-3 md:grid-cols-[1fr_auto]"> <div class="grid gap-3 md:grid-cols-[1fr_auto]">
@@ -111,7 +106,7 @@ function decrementProduct(productId: string) {
<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="filteredProducts.length > 0" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> <div v-else-if="filteredProducts.length > 0" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<article class="surface-card overflow-hidden rounded-3xl border border-dashed border-base-300 p-4"> <article class="surface-card overflow-hidden rounded-3xl p-4">
<div class="flex h-full flex-col justify-between gap-4"> <div class="flex h-full flex-col justify-between gap-4">
<div> <div>
<div class="badge badge-outline">Кастом</div> <div class="badge badge-outline">Кастом</div>