Improve mobile storefront layout, navigation, and product browsing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-11 15:02:41 +03:30
co-authored by Cursor
parent b3df0d3eb4
commit 4e4dd731b2
38 changed files with 1571 additions and 332 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ export default async function AboutPage() {
);
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductListHero
+65
View File
@@ -0,0 +1,65 @@
import type { Metadata } from "next";
import Link from "next/link";
import SiteHeader from "@/components/home/SiteHeader";
import SiteFooter from "@/components/home/SiteFooter";
import PartnerCta from "@/components/home/PartnerCta";
import ProductListHero from "@/components/products/ProductListHero";
import { mapHomeSlots } from "@/lib/map-home-slots";
import { fetchPublicWebsiteSlots } from "@/lib/website-slots";
import {
fetchPublicCategories,
mapCategoriesToMegaTabs,
} from "@/lib/categories-api";
export const metadata: Metadata = {
title: "سبد خرید | مانا کالکشن",
description: "سبد خرید مانا کالکشن",
};
export default async function CartPage() {
const [slotItems, apiCategories] = await Promise.all([
fetchPublicWebsiteSlots(),
fetchPublicCategories(),
]);
const home = mapHomeSlots(slotItems);
const megaTabs = mapCategoriesToMegaTabs(
apiCategories,
home.megaTabs.map((t) => ({ image: t.image })),
);
return (
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductListHero
titleFa="سبد خرید"
titleEn="Cart"
heroImage={home.heroSlides[0]?.image || "/images/home/hero/1.jpg"}
breadcrumbs={[
{ label: "خانه", href: "/" },
{ label: "سبد خرید" },
]}
/>
</div>
<section className="mx-auto max-w-[1200px] px-4 py-16 text-center sm:px-6 sm:py-20 lg:px-8">
<p className="font-doran text-xl font-medium text-ink sm:text-2xl">
سبد خرید شما خالی است
</p>
<p className="mx-auto mt-3 max-w-md text-sm font-light leading-7 text-muted sm:text-base">
محصولی به سبد اضافه نشده است. از کالکشن‌ها دیدن کنید و انتخاب خود را
شروع کنید.
</p>
<Link
href="/products"
className="mt-8 inline-flex h-11 items-center justify-center rounded-[4px] border border-ink bg-ink px-8 text-sm font-light text-white transition-opacity hover:opacity-85"
>
مشاهده محصولات
</Link>
</section>
<PartnerCta />
<SiteFooter paymentBadges={home.paymentBadges} />
</main>
);
}
+1 -1
View File
@@ -174,7 +174,7 @@ export default async function CategoryPage({ params, searchParams }: PageProps)
);
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductListHero
+1 -1
View File
@@ -31,7 +31,7 @@ export default async function CategoriesIndexPage() {
);
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductListHero
+1 -1
View File
@@ -30,7 +30,7 @@ export default async function ContactPage() {
);
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductListHero
+390 -1
View File
@@ -1,7 +1,7 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--background: #f5f5f5;
--foreground: #0a0a0a;
--surface: #ffffff;
--muted: #6b6b6b;
@@ -235,6 +235,395 @@ body {
}
}
/* Sticky close-up: scroll pulls back, then copy + button appear over the image. */
.opening-track {
view-timeline-name: --opening;
view-timeline-axis: block;
}
.opening-media,
.opening-shade,
.opening-headline,
.opening-copy {
animation-timing-function: linear;
animation-fill-mode: both;
animation-timeline: --opening;
animation-duration: auto;
}
.opening-media {
animation-name: opening-media;
animation-range: cover 0% cover 68%;
}
.opening-headline {
animation-name: opening-headline;
animation-range: cover 42% cover 56%;
}
.opening-shade {
animation-name: opening-shade;
animation-range: cover 46% cover 64%;
}
.opening-copy {
animation-name: opening-copy, opening-copy-hit;
animation-timing-function: linear, step-end;
animation-range: cover 52% cover 70%, cover 52% cover 70%;
}
@keyframes opening-media {
from {
transform: scale(1.38);
}
to {
transform: scale(1.06);
}
}
@keyframes opening-headline {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes opening-shade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes opening-copy {
from {
opacity: 0;
transform: translate3d(0, 28px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes opening-copy-hit {
0%,
65% {
pointer-events: none;
}
100% {
pointer-events: auto;
}
}
@supports not (animation-timeline: view()) {
.opening-track {
height: auto;
}
.opening-stage {
position: relative;
top: auto;
height: calc(100dvh - 4.5rem);
}
.opening-media,
.opening-shade,
.opening-headline,
.opening-copy {
animation: none;
}
.opening-media {
transform: scale(1.06);
}
.opening-shade {
opacity: 0.55;
}
.opening-headline {
opacity: 0;
}
.opening-copy {
opacity: 1;
transform: none;
pointer-events: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.opening-track {
height: auto;
view-timeline-name: none;
}
.opening-stage {
position: relative;
top: auto;
height: min(86dvh, 920px);
}
.opening-media,
.opening-shade,
.opening-headline,
.opening-copy {
animation: none;
}
.opening-media {
transform: scale(1.06);
}
.opening-shade {
opacity: 0.55;
}
.opening-headline {
opacity: 0;
}
.opening-copy {
opacity: 1;
transform: none;
pointer-events: auto;
}
}
/* Four-image sticky story (Ayoubi-style): scroll advances full-bleed slides. */
.sticky-story {
--story: 0;
--story-header-h: 4rem;
--frame-inset: 1.6rem;
--story-accent: #e8e2d8;
height: calc((100dvh - var(--story-header-h)) * 4);
background: #000;
}
@media (min-width: 640px) {
.sticky-story {
--story-header-h: 4.5rem;
}
}
.sticky-story__stage {
position: sticky;
top: var(--story-header-h);
height: calc(100dvh - var(--story-header-h));
overflow: hidden;
background: #000;
}
.sticky-story__slide {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
}
.sticky-story__slide.is-under {
opacity: 1;
z-index: 0;
}
.sticky-story__slide.is-under .sticky-story__copy {
opacity: 0;
}
.sticky-story__slide.is-on {
z-index: 1;
opacity: 1;
animation: sticky-story-fade 0.8s ease;
}
.sticky-story__media,
.sticky-story__shade,
.sticky-story__copy {
position: absolute;
inset: 0;
}
.sticky-story__media {
display: block;
}
.sticky-story__shade {
background:
linear-gradient(
to left,
rgba(7, 7, 8, 0.82) 0%,
rgba(7, 7, 8, 0.48) 38%,
rgba(7, 7, 8, 0.18) 68%
),
linear-gradient(to top, rgba(7, 7, 8, 0.45) 0%, transparent 42%),
linear-gradient(to right, rgba(7, 7, 8, 0.35), transparent 30%);
pointer-events: none;
}
.sticky-story__plate {
position: absolute;
z-index: 2;
inset: var(--frame-inset);
border: 1px solid var(--story-accent);
pointer-events: none;
}
.sticky-story__copy {
z-index: 3;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 0.85rem;
max-width: 38rem;
padding: 2rem clamp(2rem, 7vw, 6rem);
text-align: start;
color: #f3eee6;
pointer-events: none;
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45);
}
.sticky-story__slide.is-on .sticky-story__copy {
pointer-events: auto;
}
.sticky-story__cta {
margin-top: 0.6rem;
}
.sticky-story__kicker {
margin: 0;
font-family: var(--font-montserrat), system-ui, sans-serif;
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(243, 238, 230, 0.72);
}
.sticky-story__copy h2 {
margin: 0;
font-size: clamp(2.1rem, 5vw, 3.5rem);
font-weight: 500;
line-height: 1.3;
color: #f5f5f7;
}
.sticky-story__lead {
margin: 0;
max-width: 24rem;
font-size: 0.95rem;
font-weight: 300;
line-height: 1.8;
color: rgba(243, 238, 230, 0.86);
}
.sticky-story__rail {
position: absolute;
z-index: 2;
top: 50%;
inset-inline-end: calc(var(--frame-inset) + 1.15rem);
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 0.9rem;
margin: 0;
padding: 0;
list-style: none;
pointer-events: none;
}
.sticky-story__rail li {
font-family: var(--font-montserrat), system-ui, sans-serif;
font-size: 0.72rem;
letter-spacing: 0.16em;
color: rgba(243, 238, 230, 0.45);
}
.sticky-story__rail li.is-on {
color: var(--story-accent);
}
@keyframes sticky-story-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (max-width: 767px) {
.sticky-story {
--frame-inset: 0.75rem;
}
.sticky-story__shade {
background: linear-gradient(
to top,
rgba(7, 7, 8, 0.9) 0%,
rgba(7, 7, 8, 0.55) 34%,
rgba(7, 7, 8, 0.08) 62%
);
}
.sticky-story__copy {
justify-content: flex-end;
max-width: none;
padding: calc(var(--frame-inset) + 1.35rem)
calc(var(--frame-inset) + 1.25rem)
calc(var(--frame-inset) + 2.1rem);
}
.sticky-story__copy h2 {
font-size: 1.9rem;
}
.sticky-story__rail {
top: calc(var(--frame-inset) + 1.1rem);
bottom: auto;
inset-inline-end: calc(var(--frame-inset) + 1.35rem);
transform: none;
flex-direction: row;
gap: 0.75rem;
}
}
@media (prefers-reduced-motion: reduce) {
.sticky-story {
height: auto;
}
.sticky-story__stage {
position: relative;
top: auto;
height: auto;
overflow: visible;
}
.sticky-story__slide,
.sticky-story__slide.is-on,
.sticky-story__slide.is-under {
position: relative;
min-height: min(78dvh, 720px);
opacity: 1;
animation: none;
}
.sticky-story__slide.is-under .sticky-story__copy {
opacity: 1;
}
.sticky-story__rail {
display: none;
}
}
.mana-canvas {
background:
radial-gradient(ellipse 80% 60% at 20% 20%, rgba(0, 0, 0, 0.02), transparent 55%),
+13 -14
View File
@@ -1,5 +1,6 @@
import SiteHeader from "@/components/home/SiteHeader";
import HeroSlider from "@/components/home/HeroSlider";
import OpeningFrame from "@/components/home/OpeningFrame";
// import CategoryStrip from "@/components/home/CategoryStrip";
import CategoryBento from "@/components/home/CategoryBento";
import ProductRow from "@/components/home/ProductRow";
@@ -8,18 +9,15 @@ import PartnerCta from "@/components/home/PartnerCta";
import FeaturesBar from "@/components/home/FeaturesBar";
import BlackCollection from "@/components/home/BlackCollection";
import AboutSection from "@/components/home/AboutSection";
import StickyStory from "@/components/home/StickyStory";
import SiteFooter from "@/components/home/SiteFooter";
import SubscribeModal from "@/components/home/SubscribeModal";
import { mapHomeSlots } from "@/lib/map-home-slots";
import {
fetchPublicWebsiteSlots,
itemsForSlot,
} from "@/lib/website-slots";
import { fetchPublicWebsiteSlots } from "@/lib/website-slots";
import { getStorefrontCatalog } from "@/lib/products-api";
import {
fetchPublicCategories,
mapCategoriesToMegaTabs,
resolveHomeCategoryStrip,
} from "@/lib/categories-api";
export default async function HomePage() {
@@ -43,22 +41,16 @@ export default async function HomePage() {
})),
);
const categories = resolveHomeCategoryStrip(
apiCategories,
home.categories,
itemsForSlot(slotItems, "category-strip").length > 0,
);
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<SubscribeModal />
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<HeroSlider slides={home.heroSlides} />
</div>
{/* <CategoryStrip categories={categories} /> */}
<CategoryBento categories={categories} />
{/* Curated bento art from website slots (with local transparent defaults). */}
<CategoryBento categories={home.categories} />
<div className="flex flex-col gap-10 lg:gap-14">
<ProductRow
@@ -84,6 +76,12 @@ export default async function HomePage() {
stagger
promoSlot={home.promoDiscount}
/>
<OpeningFrame
image={home.openingFrame.image}
imageAlt={home.openingFrame.imageAlt}
/>
<FeaturesBar />
<BlackCollection items={home.blackItems} />
<ProductRow
@@ -103,6 +101,7 @@ export default async function HomePage() {
image={home.about.image}
imageSecondary={home.about.imageSecondary}
/>
<StickyStory steps={home.stickyStory} />
<SiteFooter paymentBadges={home.paymentBadges} />
</main>
);
+1 -1
View File
@@ -89,7 +89,7 @@ export default async function ProductDetailPage({ params }: PageProps) {
.slice(0, 8);
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductDetailHero
+1 -1
View File
@@ -124,7 +124,7 @@ export default async function ProductsPage({ searchParams }: PageProps) {
: LIST_PAGE_META.breadcrumbs;
return (
<main className="home-page min-h-dvh bg-white text-ink">
<main className="home-page min-h-dvh bg-background text-ink">
<div className="relative">
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
<ProductListHero