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 && (
@@ -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}
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 ? (