1235 lines
29 KiB
Vue
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>
|