Files
nademitex_user/src/views/home/HomeView.vue
T

1235 lines
29 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useCrmStore } from '@/stores/crm'
import { useFavoritesStore } from '@/stores/favorites'
import { fetchAgentProfile, fetchBanners, type AppBannerDto } from '@/services/agentApi'
import { fetchMySuggestions, type ProductSuggestionDto } from '@/services/suggestionsApi'
import IconBell from '@/components/icons/IconBell.vue'
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import levelMedalIcon from '@/assets/images/level-medal.webp'
// import quickClubCrownIcon from '@/assets/images/quick-club-crown.webp'
import quickCategoriesGridIcon from '@/assets/images/quick-categories-grid.webp'
import quickCatalogRequestIcon from '@/assets/images/quick-catalog-request.webp'
import latestOrderStatus from '@/assets/images/latest-order-status.webp'
import quickNewProductsIcon from '@/assets/images/quick-new-products.webp'
import IconHeart from '@/components/icons/IconHeart.vue'
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
import { formatNumber } from '@/utils/format'
import type { CatalogProductCard } from '@/types/crm'
import type { Component } from 'vue'
import { useGoBack } from '@/composables/useGoBack'
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
import UserAvatar from '@/components/ui/UserAvatar.vue'
const router = useRouter()
const { goBack } = useGoBack('/')
const auth = useAuthStore()
const crm = useCrmStore()
const favorites = useFavoritesStore()
const loading = ref(true)
const error = ref<string | null>(null)
const newest = ref<CatalogProductCard[]>([])
const suggestions = ref<ProductSuggestionDto[]>([])
const favError = ref<string | null>(null)
const agentLevel = ref<{
label: string
nextLabel: string
pointsRemaining: number
progressPercent: number
} | null>(null)
type QuickAction = {
id: string
label: string
to: string
icon?: Component
image?: string
}
const customerActions: QuickAction[] = [
{ id: 'qa-new', label: 'محصولات جدید', to: '/products', image: quickNewProductsIcon },
{ id: 'qa-cat', label: 'دسته بندی', to: '/categories', image: quickCategoriesGridIcon },
{ id: 'qa-club', label: 'آخرین وضعیت سفارش', to: '/orders/latest-status', image:latestOrderStatus },
{ id: 'qa-swatch', label: 'آخرین درخواست کالیته', to: '/catalog-requests/latest-status', image: quickCatalogRequestIcon },
]
const agentActions: QuickAction[] = [
{ id: 'qa-new', label: 'محصولات جدید', to: '/products', image: quickNewProductsIcon },
{ id: 'qa-cat', label: 'دسته بندی', to: '/categories', image: quickCategoriesGridIcon },
{ id: 'qa-customers', label: 'آخرین وضعیت سفارش', to: '/orders/latest-status', image: latestOrderStatus },
{ id: 'qa-swatch', label: 'آخرین درخواست کالیته', to: '/catalog-requests/latest-status', image: quickCatalogRequestIcon },
]
const actions = computed(() => (auth.isAgent ? agentActions : customerActions))
const FALLBACK_BANNER: AppBannerDto = {
id: 'fallback',
slot: 'home_hero',
title: 'کالکشن پاییز و زمستان',
subtitle: 'جدیدترین پارچه‌ها',
ctaLabel: 'مشاهده کالکشن',
ctaLink: '/categories',
imageUrl: '',
showTitle: true,
showSubtitle: true,
showCta: true,
}
const banners = ref<AppBannerDto[]>([FALLBACK_BANNER])
const bannerIndex = ref(0)
const heroAnim = ref<'hero-next' | 'hero-prev'>('hero-next')
let bannerTimer: number | undefined
const banner = computed(() => banners.value[bannerIndex.value] ?? FALLBACK_BANNER)
const heroHasCopy = computed(() => {
const item = banner.value
return (
(item.showTitle !== false && item.title) ||
(item.showSubtitle !== false && item.subtitle) ||
(item.showCta !== false && item.ctaLabel)
)
})
function slideStyle(item: AppBannerDto) {
if (!item.imageUrl) return undefined
return { '--hero-image': `url("${item.imageUrl}")` }
}
function moveBanner(index: number, dir?: 1 | -1) {
const len = banners.value.length
if (!len) return
const next = ((index % len) + len) % len
if (next === bannerIndex.value) return
if (dir === 1) heroAnim.value = 'hero-next'
else if (dir === -1) heroAnim.value = 'hero-prev'
else if (bannerIndex.value === len - 1 && next === 0) heroAnim.value = 'hero-next'
else if (bannerIndex.value === 0 && next === len - 1) heroAnim.value = 'hero-prev'
else heroAnim.value = next > bannerIndex.value ? 'hero-next' : 'hero-prev'
bannerIndex.value = next
}
function startBannerRotation() {
stopBannerRotation()
if (banners.value.length < 2) return
bannerTimer = window.setInterval(() => {
moveBanner(bannerIndex.value + 1, 1)
}, 5500)
}
function stopBannerRotation() {
if (bannerTimer) window.clearInterval(bannerTimer)
bannerTimer = undefined
}
function setBanner(index: number) {
moveBanner(index)
startBannerRotation()
}
const displayFirst = computed(() => auth.user?.firstName || 'کاربر')
const displayLast = computed(() => auth.user?.lastName || '')
const homeLevel = computed(() => {
if (agentLevel.value) return agentLevel.value
const u = auth.user
return {
label: u?.levelLabel || 'برنزی',
nextLabel: u?.nextLevelLabel || 'نقره‌ای',
pointsRemaining: u?.pointsToNext ?? 0,
progressPercent: u?.levelProgress ?? 0,
}
})
onUnmounted(stopBannerRotation)
onMounted(async () => {
loading.value = true
error.value = null
try {
const [products, homeBanners] = await Promise.all([
crm.loadBrowseProducts(),
fetchBanners('home_hero').catch(() => [] as AppBannerDto[]),
favorites.hydrate(),
])
if (homeBanners.length) banners.value = homeBanners
startBannerRotation()
newest.value = products.filter((p) => p.isNew).slice(0, 6)
if (!newest.value.length) newest.value = products.slice(0, 6)
if (auth.isAgent) {
try {
const profile = await fetchAgentProfile()
agentLevel.value = {
label: profile.levelLabel,
nextLabel: profile.nextLevelLabel,
pointsRemaining: profile.pointsToNext,
progressPercent: profile.levelProgress,
}
} catch {
// سطح از /auth/me؛ عدم دیپلوی agent API خانه را خراب نکند
}
} else {
try {
suggestions.value = await fetchMySuggestions()
} catch {
suggestions.value = []
}
}
} catch (e) {
error.value = e instanceof Error ? e.message : 'بارگذاری خانه ناموفق بود.'
} finally {
loading.value = false
}
})
function go(to: string) {
if (/^https?:\/\//i.test(to)) {
window.location.assign(to)
return
}
router.push(to)
}
function goBanner() {
go(banner.value.ctaLink.trim() || '/categories')
}
async function toggleFavorite(productId: string) {
favError.value = null
try {
await favorites.toggle(productId)
} catch (e) {
favError.value = e instanceof Error ? e.message : 'ذخیره علاقه‌مندی ناموفق بود.'
}
}
</script>
<template>
<div class="page home-page">
<header class="home-header mobile-only">
<button type="button" class="icon-btn" aria-label="بازگشت" @click="goBack">
<IconChevronBack :size="20" />
</button>
<div class="home-header__user">
<UserAvatar
class="home-header__avatar"
:src="auth.user?.avatarUrl"
:initials="displayFirst.charAt(0)"
:size="44"
/>
<div>
<p class="home-header__hello">
سلام، {{ displayFirst }} {{ displayLast }}
</p>
<p class="home-header__welcome">خوش آمدید</p>
</div>
</div>
<div class="home-header__actions">
<AuthThemeToggle variant="inline" />
<button type="button" class="icon-btn" aria-label="اعلان‌ها" @click="go('/profile/notifications')">
<IconBell :size="20" />
</button>
</div>
</header>
<header class="home-desk-head desktop-only">
<div>
<h1 class="home-desk-head__title">خانه</h1>
<p class="home-desk-head__sub">
سلام {{ displayFirst }} {{ displayLast }} — خوش آمدید
</p>
</div>
</header>
<div class="page__scroll page__scroll--with-nav home-scroll">
<p v-if="error" class="home-error">{{ error }}</p>
<PageSkeleton v-else-if="loading" variant="home" />
<template v-else>
<section class="hero-banner" :aria-label="banner.title">
<TransitionGroup :name="heroAnim" tag="div" class="hero-banner__stage">
<article
:key="banner.id"
class="hero-slide"
:class="{ 'hero-slide--copy': heroHasCopy }"
:style="slideStyle(banner)"
>
<div class="hero-slide__media" aria-hidden="true" />
<div class="hero-banner__content">
<h2 v-if="banner.showTitle !== false" class="hero-banner__title">
{{ banner.title }}
<span v-if="banner.showSubtitle !== false && banner.subtitle">{{ banner.subtitle }}</span>
</h2>
<p
v-else-if="banner.showSubtitle !== false && banner.subtitle"
class="hero-banner__title"
>
<span>{{ banner.subtitle }}</span>
</p>
<button
v-if="banner.showCta !== false && banner.ctaLabel"
type="button"
class="hero-banner__cta"
@click="goBanner"
>
{{ banner.ctaLabel }}
</button>
</div>
</article>
</TransitionGroup>
<div v-if="banners.length > 1" class="hero-banner__dots" role="tablist" aria-label="بنرها">
<button
v-for="(item, index) in banners"
:key="item.id"
type="button"
class="dot"
:class="{ 'dot--active': index === bannerIndex }"
:aria-label="item.title"
:aria-selected="index === bannerIndex"
@click="setBanner(index)"
/>
</div>
</section>
<section class="quick-grid" aria-label="دسترسی سریع">
<button
v-for="action in actions"
:key="action.id"
type="button"
class="quick-card"
@click="go(action.to)"
>
<span class="quick-card__icon" aria-hidden="true">
<img v-if="action.image" class="quick-card__image" :src="action.image" alt="" />
<component v-else :is="action.icon" :size="20" />
</span>
<span class="quick-card__label">{{ action.label }}</span>
</button>
</section>
<section v-if="auth.isAgent" class="level-card" aria-label="سطح عضویت">
<div class="level-card__top">
<div class="level-card__identity">
<img
class="level-card__medal"
:src="levelMedalIcon"
alt=""
width="48"
height="48"
aria-hidden="true"
/>
<div class="level-card__text">
<span class="level-card__eyebrow">سطح شما</span>
<strong class="level-card__name">{{ homeLevel.label }}</strong>
<span class="level-card__next">تا سطح {{ homeLevel.nextLabel }}</span>
</div>
</div>
<button type="button" class="level-card__details" @click="go('/profile')">
جزئیات سطح
</button>
</div>
<div class="level-card__progress">
<div
class="level-card__bar"
role="progressbar"
:aria-valuenow="homeLevel.progressPercent"
aria-valuemin="0"
aria-valuemax="100"
>
<span
class="level-card__bar-fill"
:style="{ width: `${homeLevel.progressPercent}%` }"
/>
</div>
<span class="level-card__points">
{{ formatNumber(homeLevel.pointsRemaining) }} امتیاز دیگر
</span>
</div>
</section>
<section v-else class="suggest-card" aria-label="پیشنهاد اختصاصی">
<div class="suggest-card__head">
<h3 class="suggest-card__title">پیشنهاد اختصاصی ما برای شما</h3>
<p v-if="!suggestions.length" class="suggest-card__empty">
هنوز پیشنهاد اختصاصی برای شما ثبت نشده است.
</p>
</div>
<div v-if="suggestions.length" class="suggest-card__row scroll-x">
<article
v-for="(item, index) in suggestions"
:key="item.id"
class="suggest-card__item"
@click="go(`/product?id=${item.productId}`)"
>
<div
class="suggest-card__image"
:class="`suggest-card__image--${index % 3}`"
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
>
<span v-if="item.isNew" class="suggest-card__badge">جدید</span>
</div>
<div class="suggest-card__body">
<strong class="suggest-card__name">{{ item.productName }}</strong>
<span class="suggest-card__sub">
{{ item.subtitle || item.catalogName || item.productCode }}
</span>
<p v-if="item.note" class="suggest-card__note">{{ item.note }}</p>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section__head">
<h3 class="section__title">جدیدترین محصولات</h3>
<button type="button" class="section__link" @click="go('/products')">
مشاهده همه
<IconChevronLeft :size="14" />
</button>
</div>
<div class="product-row scroll-x">
<article
v-for="(item, index) in newest"
:key="item.id"
class="product-card"
@click="go(`/product?id=${item.id}`)"
>
<div
class="product-card__image"
:class="`product-card__image--${index % 3}`"
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
>
<span v-if="item.isNew" class="product-card__badge">جدید</span>
</div>
<div class="product-card__body">
<strong class="product-card__name">{{ item.name }}</strong>
<span class="product-card__sub">{{ item.subtitle }}</span>
<button
type="button"
class="product-card__fav"
:class="{ 'product-card__fav--on': favorites.isFavorite(item.id) }"
:aria-label="
favorites.isFavorite(item.id)
? 'حذف از علاقه‌مندی‌ها'
: 'افزودن به علاقه‌مندی‌ها'
"
:aria-pressed="favorites.isFavorite(item.id)"
@click.stop="toggleFavorite(item.id)"
>
<IconHeart
:size="16"
:filled="favorites.isFavorite(item.id)"
:color="favorites.isFavorite(item.id) ? 'var(--color-primary)' : '#b0b7c0'"
/>
</button>
</div>
</article>
</div>
</section>
</template>
</div>
</div>
</template>
<style scoped>
.home-page {
background: var(--color-bg);
}
.home-error {
margin: 0;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.home-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px var(--page-padding) 8px;
background: var(--color-surface);
color: var(--color-text);
}
.home-header__user {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.home-header__avatar {
background: var(--color-primary-soft);
color: var(--color-primary);
}
.home-header__hello {
margin: 0;
font-size: 0.95rem;
font-weight: 700;
}
.home-header__welcome {
margin: 2px 0 0;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.home-header__actions {
display: flex;
align-items: center;
gap: 0;
color: inherit;
}
.icon-btn {
position: relative;
width: 40px;
height: 40px;
border: none;
border-radius: 12px;
background: transparent;
color: var(--color-text);
display: grid;
place-items: center;
cursor: pointer;
}
.home-scroll {
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px var(--page-padding) 8px;
}
.home-scroll.page__scroll--with-nav {
padding-bottom: 8px;
}
.hero-banner {
--hero-image: #8a7a65;
position: relative;
min-height: 160px;
border-radius: var(--radius-lg);
color: #fff;
overflow: hidden;
isolation: isolate;
}
.hero-banner__stage {
position: absolute;
inset: 0;
}
.hero-slide {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
padding: 20px;
z-index: 1;
}
.hero-slide__media {
position: absolute;
inset: 0;
z-index: 0;
background: var(--hero-image) center / cover no-repeat;
background-color: #8a7a65;
}
.hero-slide--copy .hero-slide__media::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.32) 0%, transparent 52%);
pointer-events: none;
}
.hero-banner__content {
position: relative;
z-index: 1;
width: auto;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
text-align: start;
animation: hero-copy 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.hero-next-enter-active,
.hero-next-leave-active,
.hero-prev-enter-active,
.hero-prev-leave-active {
transition:
opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.85s cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-next-leave-active,
.hero-prev-leave-active {
z-index: 1;
}
.hero-next-enter-active,
.hero-prev-enter-active {
z-index: 2;
}
.hero-next-enter-from {
opacity: 0;
transform: translate3d(-16%, 0, 0);
}
.hero-next-leave-to {
opacity: 0;
transform: translate3d(12%, 0, 0);
}
.hero-prev-enter-from {
opacity: 0;
transform: translate3d(16%, 0, 0);
}
.hero-prev-leave-to {
opacity: 0;
transform: translate3d(-12%, 0, 0);
}
@keyframes hero-copy {
from {
opacity: 0;
transform: translate3d(0, 12px, 0);
}
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.hero-banner__content,
.hero-next-enter-active,
.hero-next-leave-active,
.hero-prev-enter-active,
.hero-prev-leave-active {
animation: none;
transition: opacity 0.2s ease;
}
.hero-next-enter-from,
.hero-next-leave-to,
.hero-prev-enter-from,
.hero-prev-leave-to {
transform: none;
}
}
.hero-banner__title {
margin: 0 0 14px;
font-size: 1.05rem;
line-height: 1.7;
max-width: 16rem;
font-weight: 700;
overflow-wrap: break-word;
}
.hero-banner__title span {
display: block;
font-weight: 500;
opacity: 0.95;
}
.hero-banner__cta {
border: none;
border-radius: 999px;
background: var(--color-accent-navy);
color: #fff;
padding: 10px 16px;
font-weight: 600;
cursor: pointer;
}
.hero-banner__dots {
position: absolute;
bottom: 12px;
inset-inline-start: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
z-index: 3;
}
.dot {
width: 6px;
height: 6px;
border: none;
padding: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.45);
cursor: pointer;
transition: width 0.35s ease, background 0.35s ease;
}
.dot--active {
width: 16px;
background: #fff;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.quick-card {
border: none;
background: var(--color-surface);
border-radius: var(--radius-md);
padding: 12px 6px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
box-shadow: 0 2px 10px rgba(16, 24, 40, 0.06);
}
.quick-card__icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: grid;
place-items: center;
background: transparent;
box-shadow: none;
border: none;
}
.quick-card__image {
width: 32px;
height: 32px;
display: block;
object-fit: contain;
}
.quick-card__label {
font-weight: bold;
font-size: 0.7rem;
text-align: center;
line-height: 1.4;
}
.level-card {
background: var(--color-surface);
border-radius: 18px;
padding: 16px;
box-shadow: none;
border: 1px solid var(--color-border);
}
.level-card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.level-card__identity {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.level-card__medal {
width: 48px;
height: 48px;
flex-shrink: 0;
display: block;
object-fit: contain;
}
.level-card__text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.level-card__eyebrow,
.level-card__next {
font-size: 0.75rem;
line-height: 1.4;
color: var(--color-text-muted);
font-weight: 500;
}
.level-card__name {
font-size: 1.15rem;
line-height: 1.35;
color: var(--color-accent-navy);
font-weight: 800;
}
.level-card__details {
flex-shrink: 0;
border: none;
border-radius: 999px;
background: #eef1f4;
color: var(--color-text-muted);
font-size: 0.75rem;
font-weight: 600;
padding: 8px 12px;
cursor: pointer;
white-space: nowrap;
}
.level-card__progress {
margin-top: 14px;
display: flex;
align-items: center;
gap: 10px;
}
.level-card__bar {
flex: 1;
min-width: 0;
height: 8px;
border-radius: 999px;
background: #e8ecf0;
overflow: hidden;
}
.level-card__bar-fill {
display: block;
height: 100%;
border-radius: 999px;
background: var(--color-accent-navy);
}
.level-card__points {
flex-shrink: 0;
font-size: 0.72rem;
color: var(--color-text-muted);
white-space: nowrap;
}
.suggest-card {
background: var(--color-surface);
border-radius: 18px;
padding: 16px;
border: 1px solid var(--color-border);
min-width: 0;
}
.suggest-card__head {
margin-bottom: 12px;
}
.suggest-card__title {
margin: 0;
font-size: 0.95rem;
font-weight: 800;
color: var(--color-accent-navy);
}
.suggest-card__empty {
margin: 8px 0 0;
font-size: 0.8rem;
color: var(--color-text-muted);
line-height: 1.5;
}
.suggest-card__row {
display: flex;
flex-wrap: nowrap;
gap: 10px;
margin-inline: -16px;
padding-inline: 16px;
padding-bottom: 4px;
scroll-snap-type: x proximity;
}
.suggest-card__item {
flex: 0 0 132px;
width: 132px;
scroll-snap-align: start;
cursor: pointer;
}
.suggest-card__image {
position: relative;
height: 92px;
border-radius: 14px;
background: linear-gradient(145deg, #e8ecf0, #d5dbe3);
background-size: cover;
background-position: center;
}
.suggest-card__image--0 {
background-color: #e7dfd6;
}
.suggest-card__image--1 {
background-color: #dfe8ea;
}
.suggest-card__image--2 {
background-color: #ebe4df;
}
.suggest-card__badge {
position: absolute;
top: 8px;
right: 8px;
padding: 2px 8px;
border-radius: 999px;
background: var(--color-primary);
color: #fff;
font-size: 0.62rem;
font-weight: 700;
}
.suggest-card__body {
padding: 8px 2px 0;
}
.suggest-card__name {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
font-size: 0.78rem;
line-height: 1.35;
margin-bottom: 2px;
}
.suggest-card__sub {
display: block;
font-size: 0.68rem;
color: var(--color-text-muted);
line-height: 1.35;
}
.suggest-card__note {
margin: 4px 0 0;
font-size: 0.65rem;
color: var(--color-primary);
line-height: 1.35;
}
.section__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.section__title {
margin: 0;
font-size: 1rem;
}
.section__link {
border: none;
background: transparent;
color: var(--color-info);
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 0.8rem;
cursor: pointer;
}
.product-row {
display: flex;
gap: 12px;
padding-bottom: 0;
margin-inline: calc(-1 * var(--page-padding));
padding-inline: var(--page-padding);
scroll-snap-type: x proximity;
}
.product-card {
flex: 0 0 148px;
display: flex;
flex-direction: column;
border: none;
background: var(--color-surface);
border-radius: 16px;
padding: 0;
text-align: start;
cursor: pointer;
overflow: hidden;
box-shadow: none;
border: 1px solid var(--color-border);
scroll-snap-align: start;
}
.product-card__image {
position: relative;
height: 128px;
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.18)),
linear-gradient(145deg, #c9b8a0 0%, #8f7a62 55%, #6d5a48 100%);
background-size: cover;
background-position: center;
}
.product-card__image--1 {
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.22)),
linear-gradient(145deg, #9aa4b2 0%, #5d6b7c 55%, #3f4a57 100%);
}
.product-card__image--2 {
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.2)),
linear-gradient(145deg, #d7c4a8 0%, #b08968 50%, #7a5a3c 100%);
}
.product-card__badge {
position: absolute;
top: 10px;
inset-inline-end: 10px;
background: var(--color-accent-navy);
color: #fff;
font-size: 0.68rem;
font-weight: 700;
line-height: 1;
padding: 5px 9px;
border-radius: 999px;
}
.product-card__body {
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
padding: 10px 12px 12px;
padding-inline-end: 34px;
min-height: 56px;
}
.product-card__name {
display: block;
width: 100%;
margin: 0;
font-size: 13px;
font-weight: 700;
line-height: 1.4;
color: #1a1a1a;
text-align: start;
}
.product-card__sub {
display: block;
width: 100%;
margin: 0;
font-size: 11px;
font-weight: 400;
line-height: 1.4;
color: #8b939e;
text-align: start;
}
.product-card__fav {
position: absolute;
bottom: 10px;
inset-inline-end: 8px;
width: 22px;
height: 22px;
border: none;
background: transparent;
padding: 0;
display: grid;
place-items: center;
cursor: pointer;
color: #b0b7c0;
}
.product-card__fav--on {
color: var(--color-primary);
}
html.dark .section__title,
html.dark .home-desk-head__title,
html.dark .level-card__name,
html.dark .suggest-card__title,
html.dark .product-card__name {
color: #fff;
}
html.dark .product-card__sub {
color: #c5d0de;
}
@media (min-width: 768px) {
.home-page {
max-width: var(--content-max-width);
margin-inline: auto;
width: 100%;
padding: 8px var(--page-padding) 0;
box-sizing: border-box;
}
.home-desk-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 8px;
padding: 0;
}
.home-desk-head__title {
margin: 0 0 4px;
font-size: 1.375rem;
font-weight: 700;
color: var(--color-text);
}
.home-desk-head__sub {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.home-scroll {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-areas:
'hero hero level'
'quick quick quick'
'products products products';
gap: 18px 20px;
padding: 8px 0 28px;
align-items: stretch;
min-width: 0;
}
.hero-banner {
grid-area: hero;
width: 100%;
max-width: none;
min-width: 0;
min-height: 0;
height: auto;
aspect-ratio: 2.1 / 1;
align-self: start;
border-radius: 20px;
}
.level-card {
grid-area: level;
padding: 20px 22px;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
}
.suggest-card {
grid-area: level;
padding: 20px 22px;
align-self: start;
min-width: 0;
width: 100%;
}
.suggest-card__row {
margin-inline: -22px;
padding-inline: 22px;
}
.quick-grid {
grid-area: quick;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.home-scroll > .section {
grid-area: products;
}
.hero-slide {
width: 100%;
box-sizing: border-box;
align-items: flex-end;
justify-content: flex-start;
padding-inline-start: 36px;
padding-inline-end: 28px;
padding-block: 24px;
}
.hero-slide__media {
background-size: cover;
background-position: left center;
background-color: #8a7a65;
}
.hero-banner__content {
max-width: min(52%, 22rem);
}
.hero-banner__title {
font-size: 1.15rem;
max-width: none;
}
.hero-next-enter-from,
.hero-next-leave-to,
.hero-prev-enter-from,
.hero-prev-leave-to {
transform: none;
}
.quick-card {
min-height: 96px;
padding: 18px 14px;
border-radius: 16px;
box-shadow: 0 3px 12px rgba(16, 24, 40, 0.07);
}
.product-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
overflow: visible;
margin-inline: 0;
padding-inline: 0;
scroll-snap-type: none;
}
.product-card {
flex: none;
width: auto;
}
.product-card__image {
height: 160px;
}
}
@media (min-width: 1200px) {
.hero-banner {
aspect-ratio: 2.2 / 1;
}
.hero-banner__title {
font-size: 1.25rem;
}
.product-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
</style>