diff --git a/public/fonts/iranyekanwebboldfanum.woff b/public/fonts/iranyekanwebboldfanum.woff new file mode 100644 index 0000000..07f9756 Binary files /dev/null and b/public/fonts/iranyekanwebboldfanum.woff differ diff --git a/public/fonts/iranyekanweblightfanum.woff b/public/fonts/iranyekanweblightfanum.woff new file mode 100644 index 0000000..9e6d42c Binary files /dev/null and b/public/fonts/iranyekanweblightfanum.woff differ diff --git a/src/app/globals.css b/src/app/globals.css index 2cfcfe1..43fe0ef 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -23,7 +23,8 @@ --color-red-dark: var(--red-dark); --color-red-light: var(--red-light); --color-red-muted: var(--red-muted); - --font-sans: var(--font-vazirmatn); + --font-sans: var(--font-iranyekan); + --font-sans-bold: var(--font-iranyekan-bold); --font-display: var(--font-syne); } @@ -38,11 +39,21 @@ html { body { background: var(--background); color: var(--foreground); - font-family: var(--font-vazirmatn), system-ui, sans-serif; + font-family: var(--font-iranyekan), system-ui, sans-serif; +} + +/* Titles use the bold face; everything else stays on the light one. */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-iranyekan-bold), var(--font-iranyekan), system-ui, sans-serif; } .font-display { - font-family: var(--font-syne), var(--font-vazirmatn), sans-serif; + font-family: var(--font-syne), var(--font-iranyekan), sans-serif; } @keyframes fade-up { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1e94339..dca80ce 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,15 +1,23 @@ import type { Metadata } from "next"; -import { Vazirmatn, Syne } from "next/font/google"; +import { Syne } from "next/font/google"; +import localFont from "next/font/local"; import "./globals.css"; import { TopBar } from "@/components/home/TopBar"; import { Header } from "@/components/home/Header"; import { Footer } from "@/components/home/Footer"; import { getBusinessInfo, getCategories, getCategoryTree } from "@/lib/meshkee"; -const vazirmatn = Vazirmatn({ - variable: "--font-vazirmatn", - subsets: ["arabic", "latin"], - weight: ["400", "500", "600", "700"], +// Body text — headings use the bold weight instead, see globals.css. +const iranyekan = localFont({ + variable: "--font-iranyekan", + src: "../../public/fonts/iranyekanweblightfanum.woff", + weight: "400", +}); + +const iranyekanBold = localFont({ + variable: "--font-iranyekan-bold", + src: "../../public/fonts/iranyekanwebboldfanum.woff", + weight: "700", }); const syne = Syne({ @@ -34,7 +42,7 @@ export default async function RootLayout({ children }: LayoutProps<"/">) { diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index 1753ad3..90a9a90 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -60,7 +60,12 @@ export default async function ProductDetailPage({ } variations={variations} storeVariants={storeVariants} - fallbackPrice={{ minPrice: product.minPrice, maxPrice: product.maxPrice }} + fallbackPrice={{ + minPrice: product.minPrice, + maxPrice: product.maxPrice, + oldPrice: product.oldPrice, + discount: product.discount, + }} inStock={product.inStock} contactPhone={contactPhone} storeName={businessInfo?.nameFa} diff --git a/src/app/products/id/[id]/page.tsx b/src/app/products/id/[id]/page.tsx new file mode 100644 index 0000000..cbc0eca --- /dev/null +++ b/src/app/products/id/[id]/page.tsx @@ -0,0 +1,15 @@ +import { notFound, redirect } from "next/navigation"; +import { getProductById } from "@/lib/meshkee"; + +export default async function ProductByIdPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const product = await getProductById(id); + + if (!product?.slug) notFound(); + + redirect(`/products/${product.slug}`); +} diff --git a/src/components/home/FeaturedCategories.tsx b/src/components/home/FeaturedCategories.tsx index 6b780fa..a7c89c3 100644 --- a/src/components/home/FeaturedCategories.tsx +++ b/src/components/home/FeaturedCategories.tsx @@ -4,7 +4,6 @@ import { Swiper, SwiperSlide } from "swiper/react"; import { FreeMode } from "swiper/modules"; import "swiper/css"; import "swiper/css/free-mode"; -import { Placeholder } from "@/components/ui/Placeholder"; import { SectionTitle } from "@/components/ui/SectionTitle"; import { EmptyState } from "@/components/ui/EmptyState"; import type { Category, StaticImageWithUrl } from "@/lib/meshkee"; @@ -13,9 +12,9 @@ function CategorySlotCard({ image }: { image: StaticImageWithUrl }) { const label = image.titleFa ?? image.titleEn ?? ""; const content = (
-
+
{/* eslint-disable-next-line @next/next/no-img-element */} - {label} + {label}
{label && ( @@ -87,13 +86,14 @@ export function FeaturedCategories({ className="animate-slide-in group flex h-full flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md" style={{ animationDelay: `${i * 60}ms` }} > - +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {cat.name} +
{cat.name} diff --git a/src/components/product/ProductPurchasePanel.tsx b/src/components/product/ProductPurchasePanel.tsx index e994d26..3436037 100644 --- a/src/components/product/ProductPurchasePanel.tsx +++ b/src/components/product/ProductPurchasePanel.tsx @@ -30,7 +30,12 @@ export function ProductPurchasePanel({ header: ReactNode; variations: ProductVariation[]; storeVariants: ProductStoreVariant[]; - fallbackPrice: { minPrice: number | null; maxPrice: number | null }; + fallbackPrice: { + minPrice: number | null; + maxPrice: number | null; + oldPrice?: number | null; + discount?: number; + }; inStock: boolean; contactPhone?: string; storeName?: string; @@ -68,6 +73,17 @@ export function ProductPurchasePanel({ ? `${formatPrice(fallbackPrice.minPrice)} - ${formatPrice(fallbackPrice.maxPrice)}` : null; + const variantOldPrice = + matchedVariant?.discountedPrice != null && + matchedVariant?.price != null && + matchedVariant.price !== matchedVariant.discountedPrice + ? matchedVariant.price + : null; + const oldPrice = hasVariations ? variantOldPrice : (fallbackPrice.oldPrice ?? null); + const discount = hasVariations + ? (oldPrice != null && price != null ? Math.round((1 - price / oldPrice) * 100) : null) + : (fallbackPrice.discount ?? null); + const stockQuantity = hasVariations ? (matchedVariant?.stockQuantity ?? 0) : null; const available = hasVariations ? Boolean(matchedVariant) && stockQuantity! > 0 : inStock; const maxQty = stockQuantity && stockQuantity > 0 ? stockQuantity : 99; @@ -137,10 +153,22 @@ export function ProductPurchasePanel({ {price != null ? ( <> -

- {priceRangeLabel ?? formatPrice(price)} - تومان -

+
+ {!priceRangeLabel && oldPrice != null && ( +
+ {formatPrice(oldPrice)} + {discount != null && ( + + {discount}٪ + + )} +
+ )} +

+ {priceRangeLabel ?? formatPrice(price)} + تومان +

+
تعداد: diff --git a/src/components/ui/Carousel.tsx b/src/components/ui/Carousel.tsx index fb13506..fc2f273 100644 --- a/src/components/ui/Carousel.tsx +++ b/src/components/ui/Carousel.tsx @@ -79,7 +79,11 @@ export function Carousel({ children, ariaLabel }: CarouselProps) { className="!px-1 !py-2" > {Children.map(children, (child) => ( - {child} + // Swiper's own CSS pins slide height to 100%, which — with no definite + // wrapper height — resolves per-slide instead of stretching to match + // the tallest sibling. height:auto (inline, so it wins) lets normal + // flex stretch equalize card heights across a row. + {child} ))}
diff --git a/src/components/ui/ProductCard.tsx b/src/components/ui/ProductCard.tsx index 6485bca..c2210b1 100644 --- a/src/components/ui/ProductCard.tsx +++ b/src/components/ui/ProductCard.tsx @@ -36,11 +36,6 @@ export function ProductCard({ fit="contain" className="aspect-square w-full rounded-lg border-0" /> - {discount && ( - - {discount}٪ - - )} {variant === "brand" && ( {brand} @@ -54,8 +49,18 @@ export function ProductCard({

{name}

-
- +
+ {oldPrice != null && ( +
+ {formatPrice(oldPrice)} + {discount != null && ( + + {discount}٪ + + )} +
+ )} +
{price != null ? ( <> {formatPrice(price)} @@ -64,12 +69,7 @@ export function ProductCard({ ) : ( تماس با فروشگاه )} - - {oldPrice && ( - - {formatPrice(oldPrice)} - - )} +
@@ -77,9 +77,15 @@ export function ProductCard({ return (
- {href ? {info} : info} + {href ? ( + + {info} + + ) : ( +
{info}
+ )}
{price != null ? (