diff --git a/.env.example b/.env.example index fe80de8..920b87a 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ -NEXT_PUBLIC_STORE_URL=http://store.mana-collection.com:3001 -NEXT_PUBLIC_API_URL=http://api.mana-collection.com:3002/api/v1 +NEXT_PUBLIC_STORE_URL=http://localhost:3001 +NEXT_PUBLIC_API_URL=http://localhost:3002/api/v1 +NEXT_PUBLIC_STORE_SLUG=mana-collection diff --git a/next.config.ts b/next.config.ts index dee0320..567680b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -12,6 +12,12 @@ const nextConfig: NextConfig = { pathname: "/images/**", }, ], + remotePatterns: [ + { + protocol: "https", + hostname: "c804387.parspack.net", + }, + ], }, }; diff --git a/public/images/home/banners/autumn-leaf.png b/public/images/home/banners/autumn-leaf.png new file mode 100644 index 0000000..7f19f5d Binary files /dev/null and b/public/images/home/banners/autumn-leaf.png differ diff --git a/public/images/home/banners/hero.jpg b/public/images/home/banners/hero.jpg new file mode 100644 index 0000000..5a32005 Binary files /dev/null and b/public/images/home/banners/hero.jpg differ diff --git a/public/images/home/banners/leather.png b/public/images/home/banners/leather.png new file mode 100644 index 0000000..aaf8fe4 Binary files /dev/null and b/public/images/home/banners/leather.png differ diff --git a/public/images/home/banners/payment-badges.png b/public/images/home/banners/payment-badges.png new file mode 100644 index 0000000..7b119a9 Binary files /dev/null and b/public/images/home/banners/payment-badges.png differ diff --git a/public/images/home/banners/sale-card-export.png b/public/images/home/banners/sale-card-export.png new file mode 100644 index 0000000..ab98c08 Binary files /dev/null and b/public/images/home/banners/sale-card-export.png differ diff --git a/public/images/home/banners/sale-script-alt.jpg b/public/images/home/banners/sale-script-alt.jpg new file mode 100644 index 0000000..0c8aba8 Binary files /dev/null and b/public/images/home/banners/sale-script-alt.jpg differ diff --git a/public/images/home/banners/sale-script.png b/public/images/home/banners/sale-script.png new file mode 100644 index 0000000..b665f84 Binary files /dev/null and b/public/images/home/banners/sale-script.png differ diff --git a/public/images/home/banners/sale-serif.png b/public/images/home/banners/sale-serif.png new file mode 100644 index 0000000..b665f84 Binary files /dev/null and b/public/images/home/banners/sale-serif.png differ diff --git a/public/images/home/black/bag.png b/public/images/home/black/bag.png new file mode 100644 index 0000000..d9e9911 Binary files /dev/null and b/public/images/home/black/bag.png differ diff --git a/public/images/home/black/coat.png b/public/images/home/black/coat.png new file mode 100644 index 0000000..8141c69 Binary files /dev/null and b/public/images/home/black/coat.png differ diff --git a/public/images/home/black/pants.png b/public/images/home/black/pants.png new file mode 100644 index 0000000..8c2cea4 Binary files /dev/null and b/public/images/home/black/pants.png differ diff --git a/public/images/home/black/shirt.png b/public/images/home/black/shirt.png new file mode 100644 index 0000000..9141b5a Binary files /dev/null and b/public/images/home/black/shirt.png differ diff --git a/public/images/home/categories/cat-1.png b/public/images/home/categories/cat-1.png new file mode 100644 index 0000000..591ba79 Binary files /dev/null and b/public/images/home/categories/cat-1.png differ diff --git a/public/images/home/categories/cat-2.png b/public/images/home/categories/cat-2.png new file mode 100644 index 0000000..3cd4971 Binary files /dev/null and b/public/images/home/categories/cat-2.png differ diff --git a/public/images/home/categories/cat-3.png b/public/images/home/categories/cat-3.png new file mode 100644 index 0000000..640b977 Binary files /dev/null and b/public/images/home/categories/cat-3.png differ diff --git a/public/images/home/categories/cat-4.png b/public/images/home/categories/cat-4.png new file mode 100644 index 0000000..f255c9a Binary files /dev/null and b/public/images/home/categories/cat-4.png differ diff --git a/public/images/home/categories/cat-5.png b/public/images/home/categories/cat-5.png new file mode 100644 index 0000000..19429f9 Binary files /dev/null and b/public/images/home/categories/cat-5.png differ diff --git a/public/images/home/categories/cat-6.png b/public/images/home/categories/cat-6.png new file mode 100644 index 0000000..a3fd42d Binary files /dev/null and b/public/images/home/categories/cat-6.png differ diff --git a/public/images/home/features/authentic.svg b/public/images/home/features/authentic.svg new file mode 100644 index 0000000..49bc749 --- /dev/null +++ b/public/images/home/features/authentic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/home/features/return.svg b/public/images/home/features/return.svg new file mode 100644 index 0000000..36c481c --- /dev/null +++ b/public/images/home/features/return.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/features/shipping.svg b/public/images/home/features/shipping.svg new file mode 100644 index 0000000..fb4bacb --- /dev/null +++ b/public/images/home/features/shipping.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/home/features/support.svg b/public/images/home/features/support.svg new file mode 100644 index 0000000..57a864f --- /dev/null +++ b/public/images/home/features/support.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/home/icons/arrow-cta.svg b/public/images/home/icons/arrow-cta.svg new file mode 100644 index 0000000..a0b1189 --- /dev/null +++ b/public/images/home/icons/arrow-cta.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/home/icons/bag.svg b/public/images/home/icons/bag.svg new file mode 100644 index 0000000..39fe92d --- /dev/null +++ b/public/images/home/icons/bag.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/home/icons/bookmark.svg b/public/images/home/icons/bookmark.svg new file mode 100644 index 0000000..8a52a10 --- /dev/null +++ b/public/images/home/icons/bookmark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/icons/caret-down.svg b/public/images/home/icons/caret-down.svg new file mode 100644 index 0000000..6ce7b55 --- /dev/null +++ b/public/images/home/icons/caret-down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/icons/caret-left-dark.svg b/public/images/home/icons/caret-left-dark.svg new file mode 100644 index 0000000..86fbdc1 --- /dev/null +++ b/public/images/home/icons/caret-left-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/icons/caret-left.svg b/public/images/home/icons/caret-left.svg new file mode 100644 index 0000000..2d80f6d --- /dev/null +++ b/public/images/home/icons/caret-left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/icons/caret-right.svg b/public/images/home/icons/caret-right.svg new file mode 100644 index 0000000..54b1382 --- /dev/null +++ b/public/images/home/icons/caret-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/icons/close.svg b/public/images/home/icons/close.svg new file mode 100644 index 0000000..9fe5ed9 --- /dev/null +++ b/public/images/home/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/home/icons/funnel.svg b/public/images/home/icons/funnel.svg new file mode 100644 index 0000000..f1711dc --- /dev/null +++ b/public/images/home/icons/funnel.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/home/icons/price.svg b/public/images/home/icons/price.svg new file mode 100644 index 0000000..7396d52 --- /dev/null +++ b/public/images/home/icons/price.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/home/icons/refresh.svg b/public/images/home/icons/refresh.svg new file mode 100644 index 0000000..2fad37a --- /dev/null +++ b/public/images/home/icons/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/home/icons/sale-wordmark.svg b/public/images/home/icons/sale-wordmark.svg new file mode 100644 index 0000000..e670e7e --- /dev/null +++ b/public/images/home/icons/sale-wordmark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/home/icons/search.svg b/public/images/home/icons/search.svg new file mode 100644 index 0000000..86e445a --- /dev/null +++ b/public/images/home/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/home/icons/user.svg b/public/images/home/icons/user.svg new file mode 100644 index 0000000..b79f063 --- /dev/null +++ b/public/images/home/icons/user.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/home/products/coat-beige.jpg b/public/images/home/products/coat-beige.jpg new file mode 100644 index 0000000..3b0e3b7 Binary files /dev/null and b/public/images/home/products/coat-beige.jpg differ diff --git a/public/images/home/products/coat-blue.jpg b/public/images/home/products/coat-blue.jpg new file mode 100644 index 0000000..dd71b0c Binary files /dev/null and b/public/images/home/products/coat-blue.jpg differ diff --git a/public/images/home/products/coat-check.jpg b/public/images/home/products/coat-check.jpg new file mode 100644 index 0000000..e6f7e36 Binary files /dev/null and b/public/images/home/products/coat-check.jpg differ diff --git a/public/images/home/products/coat-navy.jpg b/public/images/home/products/coat-navy.jpg new file mode 100644 index 0000000..57b126e Binary files /dev/null and b/public/images/home/products/coat-navy.jpg differ diff --git a/public/images/home/products/product-1.png b/public/images/home/products/product-1.png new file mode 100644 index 0000000..cc644c5 Binary files /dev/null and b/public/images/home/products/product-1.png differ diff --git a/public/images/home/products/product-2.png b/public/images/home/products/product-2.png new file mode 100644 index 0000000..7b119a9 Binary files /dev/null and b/public/images/home/products/product-2.png differ diff --git a/public/images/home/products/product-3.png b/public/images/home/products/product-3.png new file mode 100644 index 0000000..7f19f5d Binary files /dev/null and b/public/images/home/products/product-3.png differ diff --git a/public/images/home/products/product-4.png b/public/images/home/products/product-4.png new file mode 100644 index 0000000..8141c69 Binary files /dev/null and b/public/images/home/products/product-4.png differ diff --git a/public/images/home/products/product-5.png b/public/images/home/products/product-5.png new file mode 100644 index 0000000..8f5bcdd Binary files /dev/null and b/public/images/home/products/product-5.png differ diff --git a/public/images/home/social/instagram.svg b/public/images/home/social/instagram.svg new file mode 100644 index 0000000..01b0ce5 --- /dev/null +++ b/public/images/home/social/instagram.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/images/home/social/linkedin.svg b/public/images/home/social/linkedin.svg new file mode 100644 index 0000000..0c28cf6 --- /dev/null +++ b/public/images/home/social/linkedin.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/images/home/social/telegram.svg b/public/images/home/social/telegram.svg new file mode 100644 index 0000000..00412c2 --- /dev/null +++ b/public/images/home/social/telegram.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/images/home/social/whatsapp.svg b/public/images/home/social/whatsapp.svg new file mode 100644 index 0000000..8ce48f5 --- /dev/null +++ b/public/images/home/social/whatsapp.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..3ebd8a6 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,112 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import SiteHeader from "@/components/home/SiteHeader"; +import SiteFooter from "@/components/home/SiteFooter"; +import AboutSection from "@/components/home/AboutSection"; +import PartnerCta from "@/components/home/PartnerCta"; +import ProductListHero from "@/components/products/ProductListHero"; +import { ABOUT_PAGE } from "@/data/pages"; +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: ABOUT_PAGE.lead, +}; + +export default async function AboutPage() { + const [slotItems, apiCategories] = await Promise.all([ + fetchPublicWebsiteSlots(), + fetchPublicCategories(), + ]); + const home = mapHomeSlots(slotItems); + const megaTabs = mapCategoriesToMegaTabs( + apiCategories, + home.megaTabs.map((t) => ({ image: t.image })), + ); + + return ( +
+
+ + +
+ + + +
+
+
+

+ {ABOUT_PAGE.storyTitleFa} +

+

+ {ABOUT_PAGE.storyTitleEn} +

+

+ {ABOUT_PAGE.story} +

+ + تماس با ما + +
+
    + {ABOUT_PAGE.values.map((value) => ( +
  • +

    {value.titleFa}

    +

    + {value.titleEn} +

    +

    + {value.body} +

    +
  • + ))} +
+
+ +
+ +
+
+ + + +
+ ); +} diff --git a/src/app/categories/[...path]/page.tsx b/src/app/categories/[...path]/page.tsx new file mode 100644 index 0000000..95730f0 --- /dev/null +++ b/src/app/categories/[...path]/page.tsx @@ -0,0 +1,164 @@ +import type { Metadata } from "next"; +import { notFound, redirect } from "next/navigation"; +import SiteHeader from "@/components/home/SiteHeader"; +import SiteFooter from "@/components/home/SiteFooter"; +import LeatherBanner from "@/components/home/LeatherBanner"; +import PartnerCta from "@/components/home/PartnerCta"; +import ProductListHero from "@/components/products/ProductListHero"; +import ProductFilters from "@/components/products/ProductFilters"; +import ProductGrid from "@/components/products/ProductGrid"; +import ProductPagination from "@/components/products/ProductPagination"; +import { getCategoryPage } from "@/data/pages"; +import { mapHomeSlots } from "@/lib/map-home-slots"; +import { fetchPublicWebsiteSlots } from "@/lib/website-slots"; +import { getStorefrontProducts } from "@/lib/products-api"; +import { + fetchPublicCategories, + mapCategoriesToMegaTabs, + type ApiCategoryNode, +} from "@/lib/categories-api"; +import { categoryPath, categorySlug } from "@/lib/category-path"; + +type PageProps = { + params: Promise<{ path: string[] }>; +}; + +function findCategory( + nodes: ApiCategoryNode[], + id: string, +): ApiCategoryNode | null { + for (const node of nodes) { + if (String(node.id) === id) return node; + const child = findCategory(node.children ?? [], id); + if (child) return child; + } + return null; +} + +async function resolveCategory(id: string) { + const apiCategories = await fetchPublicCategories(); + const apiCat = findCategory(apiCategories, id); + if (apiCat) { + return { + apiCategories, + apiCat, + staticCat: null as ReturnType, + id: apiCat.id, + nameFa: apiCat.nameFa, + nameEn: apiCat.nameEn || apiCat.nameFa, + imageUrl: apiCat.imageUrl, + description: `مجموعه‌ای از محصولات ${apiCat.nameFa} در مانا کالکشن.`, + }; + } + const staticCat = getCategoryPage(id); + if (staticCat) { + return { + apiCategories, + apiCat: null, + staticCat, + id: staticCat.id, + nameFa: staticCat.nameFa, + nameEn: staticCat.nameEn, + imageUrl: null as string | null, + description: staticCat.description, + }; + } + return null; +} + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const { path } = await params; + const id = path?.[0]; + if (!id) return { title: "دسته | مانا کالکشن" }; + const resolved = await resolveCategory(id); + if (!resolved) return { title: "دسته | مانا کالکشن" }; + return { + title: `${resolved.nameFa} | مانا کالکشن`, + description: resolved.description, + }; +} + +export default async function CategoryPage({ params }: PageProps) { + const { path } = await params; + if (!path?.length || path.length > 2) notFound(); + + const [id, slug] = path; + const resolved = await resolveCategory(id); + if (!resolved) notFound(); + + const expectedSlug = categorySlug({ + id: resolved.id, + nameEn: resolved.nameEn, + nameFa: resolved.nameFa, + }); + const canonical = categoryPath({ + id: resolved.id, + nameEn: resolved.nameEn, + nameFa: resolved.nameFa, + }); + + // Legacy `/categories/:id` or wrong slug → canonical `/categories/:id/:slug` + if (path.length === 1 || slug !== expectedSlug) { + redirect(canonical); + } + + const [slotItems, products] = await Promise.all([ + fetchPublicWebsiteSlots(), + getStorefrontProducts( + resolved.apiCat ? { categoryId: resolved.id } : undefined, + ), + ]); + const home = mapHomeSlots(slotItems); + const megaTabs = mapCategoriesToMegaTabs( + resolved.apiCategories, + home.megaTabs.map((t) => ({ image: t.image })), + ); + + return ( +
+
+ + +
+ +
+

+ {resolved.description} +

+
+ +
+ +
+ + +
+
+ + + + +
+ ); +} diff --git a/src/app/categories/page.tsx b/src/app/categories/page.tsx new file mode 100644 index 0000000..c929ef0 --- /dev/null +++ b/src/app/categories/page.tsx @@ -0,0 +1,91 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +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 { LIST_PAGE_META } from "@/data/products"; +import { mapHomeSlots } from "@/lib/map-home-slots"; +import { fetchPublicWebsiteSlots } from "@/lib/website-slots"; +import { + fetchPublicCategories, + mapCategoriesToMegaTabs, + mapCategoriesToStrip, +} from "@/lib/categories-api"; +import { categoryPath } from "@/lib/category-path"; + +export const metadata: Metadata = { + title: "دسته‌بندی‌ها | مانا کالکشن", + description: "مرور دسته‌بندی‌های مانا کالکشن", +}; + +export default async function CategoriesIndexPage() { + const [slotItems, apiCategories] = await Promise.all([ + fetchPublicWebsiteSlots(), + fetchPublicCategories(), + ]); + const home = mapHomeSlots(slotItems); + const list = mapCategoriesToStrip(apiCategories); + const megaTabs = mapCategoriesToMegaTabs( + apiCategories, + home.megaTabs.map((t) => ({ image: t.image })), + ); + + return ( +
+
+ + +
+ +
+
    + {list.map((cat) => ( +
  • + +
    + {cat.nameFa} +
    +
    +
    +

    {cat.nameFa}

    +

    + {cat.nameEn} +

    +
    + + مشاهده + +
    + +
  • + ))} +
+
+ + + +
+ ); +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx new file mode 100644 index 0000000..a9123ab --- /dev/null +++ b/src/app/contact/page.tsx @@ -0,0 +1,109 @@ +import type { Metadata } from "next"; +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 ContactForm from "@/components/contact/ContactForm"; +import { CONTACT_PAGE } from "@/data/pages"; +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: CONTACT_PAGE.lead, +}; + +export default async function ContactPage() { + const [slotItems, apiCategories] = await Promise.all([ + fetchPublicWebsiteSlots(), + fetchPublicCategories(), + ]); + const home = mapHomeSlots(slotItems); + const megaTabs = mapCategoriesToMegaTabs( + apiCategories, + home.megaTabs.map((t) => ({ image: t.image })), + ); + + return ( +
+
+ + +
+ +
+
+ + +
+

+ فرم تماس +

+

+ Send a message +

+
+ +
+
+
+
+ + + +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 4ea29a0..7393aac 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,7 +1,7 @@ @import "tailwindcss"; :root { - --background: #fbfbfa; + --background: #ffffff; --foreground: #0a0a0a; --surface: #ffffff; --muted: #6b6b6b; @@ -16,13 +16,17 @@ --color-muted: var(--muted); --color-line: var(--line); --color-ink: var(--ink); - --font-sans: var(--font-doran); + --font-sans: var(--font-iranyekan); --font-display: var(--font-cailyne); } body { background: var(--background); color: var(--foreground); + font-family: var(--font-iranyekan), Tahoma, sans-serif; +} + +.font-doran { font-family: var(--font-doran), Tahoma, sans-serif; } @@ -35,6 +39,10 @@ body { font-family: var(--font-montserrat), system-ui, sans-serif; } +.home-page { + overflow-x: clip; +} + @keyframes coming-soon-rise { from { opacity: 0; @@ -157,6 +165,76 @@ body { } } +@keyframes hero-ken-burns { + from { + transform: scale(1); + } + to { + transform: scale(1.06); + } +} + +.hero-ken-burns { + animation: hero-ken-burns 10s linear forwards; + will-change: transform; + transform-origin: center center; +} + +.hero-ken-burns-paused { + animation-play-state: paused; +} + +.seq-fade-item { + transition: + opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); + will-change: opacity, transform; +} + +@media (prefers-reduced-motion: reduce) { + .seq-fade-item { + opacity: 1 !important; + transform: none !important; + transition: none !important; + } +} + +@keyframes hero-copy-rise { + from { + opacity: 0; + transform: translateY(18px); + filter: blur(6px); + } + to { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + +.hero-copy-item { + opacity: 0; + animation: hero-copy-rise 0.75s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +.hero-copy-item-2 { + animation-delay: 0.12s; +} + +.hero-copy-item-3 { + animation-delay: 0.24s; +} + +@media (prefers-reduced-motion: reduce) { + .hero-ken-burns, + .hero-copy-item { + animation: none !important; + opacity: 1 !important; + filter: none !important; + transform: none !important; + } +} + .mana-canvas { background: radial-gradient(ellipse 80% 60% at 20% 20%, rgba(0, 0, 0, 0.02), transparent 55%), diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e678929..9145061 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,48 @@ import { Montserrat } from "next/font/google"; import localFont from "next/font/local"; import "./globals.css"; +const iranYekan = localFont({ + src: [ + { + path: "../fonts/iranyekan/IRANYekanThinFaNum.ttf", + weight: "100", + style: "normal", + }, + { + path: "../fonts/iranyekan/IRANYekanLightFaNum.ttf", + weight: "300", + style: "normal", + }, + { + path: "../fonts/iranyekan/IRANYekanRegularFaNum.ttf", + weight: "400", + style: "normal", + }, + { + path: "../fonts/iranyekan/IRANYekanMediumFaNum.ttf", + weight: "500", + style: "normal", + }, + { + path: "../fonts/iranyekan/IRANYekanBoldFaNum.ttf", + weight: "700", + style: "normal", + }, + { + path: "../fonts/iranyekan/IRANYekanExtraBoldFaNum.ttf", + weight: "800", + style: "normal", + }, + { + path: "../fonts/iranyekan/IRANYekanBlackFaNum.ttf", + weight: "900", + style: "normal", + }, + ], + variable: "--font-iranyekan", + display: "swap", +}); + const doran = localFont({ src: [ { @@ -50,8 +92,9 @@ const montserrat = Montserrat({ }); export const metadata: Metadata = { - title: "به‌زودی | مانا کالکشن", - description: "فروشگاه آنلاین مانا کالکشن به‌زودی در دسترس خواهد بود.", + title: "مانا کالکشن | Mana Collection", + description: + "فروشگاه آنلاین مانا کالکشن — پوشاک زنانه، کالکشن پاییزه و انتخاب‌های خاص برای بانویی که خودش را انتخاب می‌کند.", }; export default function RootLayout({ @@ -63,7 +106,7 @@ export default function RootLayout({ {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 8ff0211..722a868 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,81 +1,109 @@ -import Image from "next/image"; -import ManaLogo from "@/components/ui/ManaLogo"; +import SiteHeader from "@/components/home/SiteHeader"; +import HeroSlider from "@/components/home/HeroSlider"; +import CategoryStrip from "@/components/home/CategoryStrip"; +import ProductRow from "@/components/home/ProductRow"; +import LeatherBanner from "@/components/home/LeatherBanner"; +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 SiteFooter from "@/components/home/SiteFooter"; +import { mapHomeSlots } from "@/lib/map-home-slots"; +import { + fetchPublicWebsiteSlots, + itemsForSlot, +} from "@/lib/website-slots"; +import { getStorefrontCatalog } from "@/lib/products-api"; +import { + fetchPublicCategories, + mapCategoriesToMegaTabs, + mapCategoriesToStrip, +} from "@/lib/categories-api"; -const STORE_LOGIN = - process.env.NEXT_PUBLIC_STORE_URL?.replace(/\/$/, "") || - "http://store.mana-collection.com:3001"; +export default async function HomePage() { + const [slotItems, catalog, apiCategories] = await Promise.all([ + fetchPublicWebsiteSlots(), + getStorefrontCatalog(), + fetchPublicCategories(), + ]); + const home = mapHomeSlots(slotItems); + + const saleProducts = catalog.promo1; + const discountProducts = catalog.promo2; + const autumnProducts = catalog.promo3; + + const stripCategories = mapCategoriesToStrip(apiCategories); + const megaTabs = mapCategoriesToMegaTabs( + apiCategories, + home.megaTabs.map((t) => ({ + image: t.image, + nameFa: t.nameFa, + href: t.href, + })), + ); + + // Prefer Website Images → category-strip slots over catalog categories + const hasWebsiteCategoryStrip = + itemsForSlot(slotItems, "category-strip").length > 0; + const categories = hasWebsiteCategoryStrip + ? home.categories + : stripCategories.length + ? stripCategories + : home.categories; -export default function HomePage() { return ( -
- {/* dir=ltr keeps content on visual left, image on visual right */} -
- {/* Content — left */} -
-
- -
-
- -
- -
-
-

- Coming soon -

-

- به‌زودی -

-

- در این آدرس میزبان شما خواهیم بود. -

-
-
- - -
-
- - {/* Image — right */} - +
+
+ +
+ + + +
+ + +
+ + +
+ + + + + +
+ + +
); } diff --git a/src/app/products/[...path]/page.tsx b/src/app/products/[...path]/page.tsx new file mode 100644 index 0000000..e5c3757 --- /dev/null +++ b/src/app/products/[...path]/page.tsx @@ -0,0 +1,122 @@ +import type { Metadata } from "next"; +import { notFound, redirect } from "next/navigation"; +import SiteHeader from "@/components/home/SiteHeader"; +import SiteFooter from "@/components/home/SiteFooter"; +import ProductDetailHero from "@/components/product-detail/ProductDetailHero"; +import ProductGallery from "@/components/product-detail/ProductGallery"; +import ProductBuyBox from "@/components/product-detail/ProductBuyBox"; +import ProductTabs from "@/components/product-detail/ProductTabs"; +import SimilarProducts from "@/components/product-detail/SimilarProducts"; +import { getProductDetail } from "@/data/product-detail"; +import { mapHomeSlots } from "@/lib/map-home-slots"; +import { fetchPublicWebsiteSlots } from "@/lib/website-slots"; +import { + fetchPublicProduct, + getStorefrontProducts, + mapApiProductToDetail, +} from "@/lib/products-api"; +import { + fetchPublicCategories, + mapCategoriesToMegaTabs, +} from "@/lib/categories-api"; +import { productPath, productSlug } from "@/lib/product-path"; + +type PageProps = { + params: Promise<{ path: string[] }>; +}; + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const { path } = await params; + const id = path?.[0]; + if (!id) return { title: "محصول | مانا کالکشن" }; + const api = await fetchPublicProduct(id); + if (api) { + return { + title: `${api.nameFa} | مانا کالکشن`, + description: api.summary || api.description || api.nameFa, + }; + } + const fallback = getProductDetail(id); + if (!fallback) return { title: "محصول | مانا کالکشن" }; + return { + title: `${fallback.nameFa} | مانا کالکشن`, + description: fallback.summary, + }; +} + +export default async function ProductDetailPage({ params }: PageProps) { + const { path } = await params; + if (!path?.length || path.length > 2) notFound(); + + const [id, slug] = path; + const [slotItems, apiProduct, similar, apiCategories] = await Promise.all([ + fetchPublicWebsiteSlots(), + fetchPublicProduct(id), + getStorefrontProducts(), + fetchPublicCategories(), + ]); + + const product = apiProduct + ? mapApiProductToDetail(apiProduct) + : getProductDetail(id); + if (!product) notFound(); + + const expectedSlug = productSlug({ + id: product.id, + nameEn: product.nameEn, + nameFa: product.nameFa, + }); + const canonical = productPath({ + id: product.id, + nameEn: product.nameEn, + nameFa: product.nameFa, + }); + + // Legacy `/products/:id` or wrong slug → canonical `/products/:id/:slug` + if (path.length === 1 || slug !== expectedSlug) { + redirect(canonical); + } + + const home = mapHomeSlots(slotItems); + const megaTabs = mapCategoriesToMegaTabs( + apiCategories, + home.megaTabs.map((t) => ({ image: t.image })), + ); + const similarCards = similar + .filter((p) => p.id !== product.id) + .slice(0, 8); + + return ( +
+
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ ); +} diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx new file mode 100644 index 0000000..34aa668 --- /dev/null +++ b/src/app/products/page.tsx @@ -0,0 +1,77 @@ +import SiteHeader from "@/components/home/SiteHeader"; +import SiteFooter from "@/components/home/SiteFooter"; +import LeatherBanner from "@/components/home/LeatherBanner"; +import PartnerCta from "@/components/home/PartnerCta"; +import ProductListHero from "@/components/products/ProductListHero"; +import ProductFilters from "@/components/products/ProductFilters"; +import ProductGrid from "@/components/products/ProductGrid"; +import ProductPagination from "@/components/products/ProductPagination"; +import { LIST_PAGE_META } from "@/data/products"; +import { mapHomeSlots } from "@/lib/map-home-slots"; +import { fetchPublicWebsiteSlots } from "@/lib/website-slots"; +import { getStorefrontProducts } from "@/lib/products-api"; +import { + fetchPublicCategories, + mapCategoriesToMegaTabs, +} from "@/lib/categories-api"; + +type PageProps = { + searchParams: Promise<{ q?: string; cat?: string }>; +}; + +export default async function ProductsPage({ searchParams }: PageProps) { + const params = await searchParams; + const [slotItems, products, apiCategories] = await Promise.all([ + fetchPublicWebsiteSlots(), + getStorefrontProducts({ + name: params.q, + categoryId: params.cat, + }), + fetchPublicCategories(), + ]); + const home = mapHomeSlots(slotItems); + const megaTabs = mapCategoriesToMegaTabs( + apiCategories, + home.megaTabs.map((t) => ({ image: t.image })), + ); + + return ( +
+
+ + +
+ +
+ +
+ + +
+
+ + + + +
+ ); +} diff --git a/src/components/contact/ContactForm.tsx b/src/components/contact/ContactForm.tsx new file mode 100644 index 0000000..6b31ca3 --- /dev/null +++ b/src/components/contact/ContactForm.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { CONTACT_PAGE } from "@/data/pages"; + +export default function ContactForm() { + const [sent, setSent] = useState(false); + const [saving, setSaving] = useState(false); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + setSaving(true); + window.setTimeout(() => { + setSaving(false); + setSent(true); + }, 500); + } + + if (sent) { + return ( +
+

پیام شما دریافت شد

+

+ Message received +

+

+ به‌زودی با شما تماس می‌گیریم. +

+ +
+ ); + } + + return ( +
+

{CONTACT_PAGE.lead}

+ +
+ + +
+ + + + + +