Add Torob tags, Meshkee SEO routes, and updated static-image slots.
Product pages emit crawler-readable meta, public URLs use id plus Farsi slugs, and the homepage catalog matches the dashboard slot keys. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f42c30a4b7
commit
c84147e765
@@ -6,12 +6,14 @@ import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
|
||||
export function AboutUs({
|
||||
image,
|
||||
images = [],
|
||||
aspectRatio,
|
||||
}: {
|
||||
image?: StaticImageWithUrl;
|
||||
images?: StaticImageWithUrl[];
|
||||
aspectRatio?: string;
|
||||
}) {
|
||||
const image = images[0];
|
||||
|
||||
return (
|
||||
<section id="about" className="container-site mt-14">
|
||||
<div className="grid md:grid-cols-2 gap-8 items-center rounded-2xl bg-zinc-50 p-5 md:p-8">
|
||||
|
||||
+25
-21
@@ -30,7 +30,7 @@ export function BannerGrid4({ images = [], aspectRatio }: BannerSlotProps) {
|
||||
className="relative overflow-hidden rounded-2xl"
|
||||
style={slotAspectStyle(aspectRatio, "21:9")}
|
||||
>
|
||||
<SlotImage image={{ ...image, linkUrl: null }} fill alt={slotAlt(image)} />
|
||||
<SlotImage image={{ ...image, linkUrl: null }} fill alt={slotAlt(image, "بنر سالار استور")} />
|
||||
{hasCopy ? (
|
||||
<>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[70%]" style={banner4Overlay} />
|
||||
@@ -85,28 +85,32 @@ export function BannerGrid4({ images = [], aspectRatio }: BannerSlotProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function BannerRow3({ images = [], aspectRatio }: BannerSlotProps) {
|
||||
export function BannerRow({ images = [], aspectRatio }: BannerSlotProps) {
|
||||
return (
|
||||
<section className="container-site mt-12">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-4">
|
||||
{images.length > 0
|
||||
? images.map((image) => (
|
||||
<div key={image.id} className="overflow-hidden rounded-2xl">
|
||||
<SlotImage
|
||||
image={image}
|
||||
alt={slotAlt(image)}
|
||||
imgClassName="h-auto w-full object-cover"
|
||||
aspectRatio={aspectRatio}
|
||||
fallbackAspect="12:5"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: banners3.map((banner) => (
|
||||
<Link key={banner.href} href={banner.href} className="overflow-hidden rounded-2xl">
|
||||
<Media src={banner.image} alt={banner.alt} className="aspect-[2.4/1]" sizes="33vw" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{images.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
|
||||
{images.map((image) => (
|
||||
<div key={image.id} className="overflow-hidden rounded-2xl">
|
||||
<SlotImage
|
||||
image={image}
|
||||
alt={slotAlt(image, "بنر سالار استور")}
|
||||
imgClassName="h-auto w-full object-cover"
|
||||
aspectRatio={aspectRatio}
|
||||
fallbackAspect="12:5"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-4">
|
||||
{banners3.map((banner) => (
|
||||
<Link key={banner.href} href={banner.href} className="overflow-hidden rounded-2xl">
|
||||
<Media src={banner.image} alt={banner.alt} className="aspect-[2.4/1]" sizes="33vw" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ export async function BlogCarousel() {
|
||||
<Carousel ariaLabel="مقالات">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/${post.slug}`}
|
||||
key={post.id || post.slug}
|
||||
href={post.href || `/blog/${post.slug}`}
|
||||
className="snap-start shrink-0 w-[240px] sm:w-[280px] rounded-2xl bg-white border border-black/5 overflow-hidden hover:border-brand/40"
|
||||
>
|
||||
<Media src={post.image} alt="" className="aspect-[16/10]" sizes="280px" />
|
||||
<Media src={post.image} alt={post.title} className="aspect-[16/10]" sizes="280px" />
|
||||
<div className="p-4">
|
||||
<time className="text-xs text-zinc-400">{post.date}</time>
|
||||
<h3 className="mt-2 text-sm leading-7 line-clamp-2">{post.title}</h3>
|
||||
|
||||
+45
-9
@@ -1,20 +1,56 @@
|
||||
import Image from "next/image";
|
||||
import { SlotImage } from "@/components/SlotImage";
|
||||
import { brands } from "@/lib/data";
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
import { Carousel } from "./Carousel";
|
||||
|
||||
export function Brands() {
|
||||
type Props = {
|
||||
images?: StaticImageWithUrl[];
|
||||
aspectRatio?: string;
|
||||
};
|
||||
|
||||
export function Brands({ images = [], aspectRatio }: Props) {
|
||||
return (
|
||||
<section className="container-site mt-12">
|
||||
<h2 className="section-title mb-6">برندها</h2>
|
||||
<Carousel ariaLabel="برندها">
|
||||
{brands.map((brand) => (
|
||||
<div
|
||||
key={brand.name}
|
||||
className="snap-start shrink-0 w-[168px] sm:w-[196px] h-[108px] rounded-xl bg-white border border-black/5 grid place-items-center px-3"
|
||||
>
|
||||
<Image src={brand.image} alt={brand.name} width={180} height={80} className="max-h-[72px] w-auto object-contain" />
|
||||
</div>
|
||||
))}
|
||||
{images.length > 0
|
||||
? images.map((image) => {
|
||||
const card = (
|
||||
<div
|
||||
className="snap-start shrink-0 w-[168px] sm:w-[196px] rounded-xl bg-white border border-black/5 grid place-items-center px-3"
|
||||
style={slotAspectStyle(aspectRatio, "2:1")}
|
||||
>
|
||||
<SlotImage
|
||||
image={{ ...image, linkUrl: null }}
|
||||
alt={slotAlt(image, "برند")}
|
||||
imgClassName="max-h-[72px] w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return image.linkUrl ? (
|
||||
<a key={image.id} href={image.linkUrl} className="snap-start shrink-0">
|
||||
{card}
|
||||
</a>
|
||||
) : (
|
||||
<div key={image.id}>{card}</div>
|
||||
);
|
||||
})
|
||||
: brands.map((brand) => (
|
||||
<div
|
||||
key={brand.name}
|
||||
className="snap-start shrink-0 w-[168px] sm:w-[196px] h-[108px] rounded-xl bg-white border border-black/5 grid place-items-center px-3"
|
||||
>
|
||||
<Image
|
||||
src={brand.image}
|
||||
alt={brand.name}
|
||||
width={180}
|
||||
height={80}
|
||||
className="max-h-[72px] w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ export function CartPopup({ open, onClose }: Props) {
|
||||
onClick={onClose}
|
||||
className="size-20 shrink-0 overflow-hidden rounded-xl border border-black/5"
|
||||
>
|
||||
<Media src={item.image || PLACEHOLDER.product} alt="" className="size-full" sizes="80px" />
|
||||
<Media src={item.image || PLACEHOLDER.product} alt={item.name} className="size-full" sizes="80px" />
|
||||
</Link>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
|
||||
+29
-12
@@ -2,11 +2,21 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { SlotImage } from "@/components/SlotImage";
|
||||
import { categories } from "@/lib/data";
|
||||
import { getCategories } from "@/lib/api";
|
||||
import { categoryHref } from "@/lib/paths";
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
import { DrawFrame } from "./DrawFrame";
|
||||
|
||||
export function Categories({ images = [] }: { images?: StaticImageWithUrl[] }) {
|
||||
export async function Categories({
|
||||
images = [],
|
||||
aspectRatio,
|
||||
}: {
|
||||
images?: StaticImageWithUrl[];
|
||||
aspectRatio?: string;
|
||||
}) {
|
||||
const apiCategories = images.length > 0 ? [] : await getCategories();
|
||||
const bySlug = new Map(apiCategories.map((item) => [item.slug, item]));
|
||||
return (
|
||||
<section className="container-site mt-12">
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
@@ -18,27 +28,31 @@ export function Categories({ images = [] }: { images?: StaticImageWithUrl[] }) {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 md:gap-4">
|
||||
{images.length > 0
|
||||
? images.map((image) => {
|
||||
const label = slotAlt(image);
|
||||
const nameFa = image.titleFa?.trim();
|
||||
const nameEn = image.titleEn?.trim();
|
||||
const card = (
|
||||
<div className="draw-frame-host relative flex items-center gap-3 rounded-2xl border border-black/5 bg-zinc-50 px-3 py-2 text-brand">
|
||||
<div className="size-[72px] sm:size-20 shrink-0 overflow-hidden" style={slotAspectStyle("1:1")}>
|
||||
<div
|
||||
className="size-[72px] sm:size-20 shrink-0 overflow-hidden"
|
||||
style={slotAspectStyle(aspectRatio, "1:1")}
|
||||
>
|
||||
<SlotImage
|
||||
image={{ ...image, linkUrl: null }}
|
||||
alt={label}
|
||||
alt={slotAlt(image, nameFa || nameEn || "دستهبندی")}
|
||||
imgClassName="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
{label || image.subtext ? (
|
||||
{nameFa || nameEn ? (
|
||||
<span className="min-w-0">
|
||||
{label ? (
|
||||
<span className="block text-lg font-medium text-ink leading-none">{label}</span>
|
||||
{nameFa ? (
|
||||
<span className="block text-lg font-medium text-ink leading-none">{nameFa}</span>
|
||||
) : null}
|
||||
{image.subtext ? (
|
||||
{nameEn ? (
|
||||
<span
|
||||
dir="ltr"
|
||||
className="mt-1 block font-oswald text-[10px] font-normal tracking-[0.32em] text-zinc-400 uppercase"
|
||||
>
|
||||
{image.subtext}
|
||||
{nameEn}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
@@ -54,10 +68,12 @@ export function Categories({ images = [] }: { images?: StaticImageWithUrl[] }) {
|
||||
<div key={image.id}>{card}</div>
|
||||
);
|
||||
})
|
||||
: categories.map((cat) => (
|
||||
: categories.map((cat) => {
|
||||
const api = bySlug.get(cat.slug);
|
||||
return (
|
||||
<Link
|
||||
key={cat.slug}
|
||||
href={`/products/${cat.slug}`}
|
||||
href={api ? categoryHref(api) : `/products/${cat.slug}`}
|
||||
className="draw-frame-host relative flex items-center gap-3 rounded-2xl border border-black/5 bg-zinc-50 px-3 py-2 text-brand"
|
||||
>
|
||||
<Image
|
||||
@@ -78,7 +94,8 @@ export function Categories({ images = [] }: { images?: StaticImageWithUrl[] }) {
|
||||
</span>
|
||||
<DrawFrame />
|
||||
</Link>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Footer() {
|
||||
<div className="container-site grid grid-cols-2 md:grid-cols-4 gap-6 py-8">
|
||||
{features.map((item) => (
|
||||
<div key={item.title} className="flex items-center gap-3">
|
||||
<Image src={item.image} alt="" width={48} height={48} className="w-12 h-12 object-contain" />
|
||||
<Image src={item.image} alt={item.title} width={48} height={48} className="w-12 h-12 object-contain" />
|
||||
<h3 className="text-sm font-medium text-ink">{item.title}</h3>
|
||||
</div>
|
||||
))}
|
||||
|
||||
+12
-12
@@ -4,7 +4,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { customerLoginUrl } from "@/lib/customer";
|
||||
import { parentCategories } from "@/lib/data";
|
||||
import type { MenuCategory } from "@/lib/nav";
|
||||
import { SITE } from "@/lib/site";
|
||||
import { useCart } from "@/lib/cart";
|
||||
import { useSession } from "@/lib/session";
|
||||
@@ -20,7 +20,7 @@ const nav = [
|
||||
{ name: "تماس با ما", href: "/contactus" },
|
||||
];
|
||||
|
||||
export function Header() {
|
||||
export function Header({ menuCategories = [] }: { menuCategories?: MenuCategory[] }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mega, setMega] = useState(false);
|
||||
const [cartOpen, setCartOpen] = useState(false);
|
||||
@@ -60,16 +60,16 @@ export function Header() {
|
||||
</Link>
|
||||
{mega && (
|
||||
<div className="absolute top-full right-0 w-[760px] rounded-2xl bg-white border border-black/5 shadow-xl p-5 grid grid-cols-4 gap-5">
|
||||
{parentCategories.map((cat) => (
|
||||
<div key={cat.slug}>
|
||||
<Link href={`/products/${cat.slug}`} className="block mb-3 text-sm font-bold text-brand">
|
||||
{menuCategories.map((cat) => (
|
||||
<div key={cat.href}>
|
||||
<Link href={cat.href} className="block mb-3 text-sm font-bold text-brand">
|
||||
{cat.name}
|
||||
</Link>
|
||||
<ul className="space-y-1.5">
|
||||
{cat.children.slice(0, 8).map((child) => (
|
||||
<li key={child.slug}>
|
||||
<li key={child.href}>
|
||||
<Link
|
||||
href={`/products/${child.slug}`}
|
||||
href={child.href}
|
||||
className="block text-[13px] text-zinc-600 hover:text-ink"
|
||||
>
|
||||
{child.name}
|
||||
@@ -131,15 +131,15 @@ export function Header() {
|
||||
<SearchBox className="mb-5 rounded-full bg-zinc-100 px-3" onNavigate={() => setOpen(false)} />
|
||||
<p className="text-xs text-zinc-400 mb-2">محصولات</p>
|
||||
<div className="space-y-4 mb-6">
|
||||
{parentCategories.map((cat) => (
|
||||
<div key={cat.slug}>
|
||||
<Link href={`/products/${cat.slug}`} className="font-bold text-brand" onClick={() => setOpen(false)}>
|
||||
{menuCategories.map((cat) => (
|
||||
<div key={cat.href}>
|
||||
<Link href={cat.href} className="font-bold text-brand" onClick={() => setOpen(false)}>
|
||||
{cat.name}
|
||||
</Link>
|
||||
<ul className="mt-2 grid grid-cols-2 gap-1">
|
||||
{cat.children.slice(0, 6).map((child) => (
|
||||
<li key={child.slug}>
|
||||
<Link href={`/products/${child.slug}`} className="text-sm text-zinc-600" onClick={() => setOpen(false)}>
|
||||
<li key={child.href}>
|
||||
<Link href={child.href} className="text-sm text-zinc-600" onClick={() => setOpen(false)}>
|
||||
{child.name}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -177,7 +177,7 @@ export function HeroSlider({
|
||||
className="relative overflow-hidden rounded-2xl"
|
||||
style={slotAspectStyle(sideAspectRatio, "13:5")}
|
||||
>
|
||||
<SlotImage image={{ ...image, linkUrl: null }} fill alt={slotAlt(image)} />
|
||||
<SlotImage image={{ ...image, linkUrl: null }} fill alt={slotAlt(image, "بنر سالار استور")} />
|
||||
{hasCopy ? (
|
||||
<>
|
||||
<div
|
||||
|
||||
@@ -8,14 +8,14 @@ export function ProductCard({ product }: { product: Product }) {
|
||||
const unavailable = product.stock === 0 || product.price === 0;
|
||||
|
||||
return (
|
||||
<Link href={product.slug ? `/products/${product.slug}` : "/products"} className="relative flex h-full min-h-[300px] flex-col overflow-hidden rounded-xl bg-white p-2">
|
||||
<Link href={product.href || "/products"} className="relative flex h-full min-h-[300px] flex-col overflow-hidden rounded-xl bg-white p-2">
|
||||
{off > 0 && (
|
||||
<span className="absolute top-2 left-2 z-[2] rounded-full bg-brand px-2.5 py-1.5 font-oswald text-[13px] font-bold leading-none text-white shadow-md">
|
||||
{off}%
|
||||
</span>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Media src={product.image} alt="" className="aspect-square rounded-lg" sizes="220px" />
|
||||
<Media src={product.image} alt={product.name} className="aspect-square rounded-lg" sizes="220px" />
|
||||
</div>
|
||||
<h3 className="mt-2 px-1 text-[13px] leading-6 text-ink line-clamp-2 min-h-12">{product.name}</h3>
|
||||
<div className="mt-auto px-1 pb-1 pt-2">
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ProductShelf({ id, image, products, ariaLabel, tone = "red" }: P
|
||||
<div className="rounded-2xl p-4 md:p-5" style={{ background: gradients[tone] }}>
|
||||
<div className="flex items-stretch gap-3">
|
||||
<div className="relative w-[150px] sm:w-[190px] shrink-0 min-h-[300px]">
|
||||
<Image src={image} alt="" fill sizes="190px" className="object-contain object-center" />
|
||||
<Image src={image} alt={ariaLabel} fill sizes="190px" className="object-contain object-center" />
|
||||
</div>
|
||||
<Carousel ariaLabel={ariaLabel} className="min-w-0 flex-1 py-1" tight>
|
||||
{products.map((product) => (
|
||||
|
||||
@@ -29,9 +29,10 @@ export function ProductView({ product, variants }: Props) {
|
||||
const [qty, setQty] = useState(1);
|
||||
const [added, setAdded] = useState(false);
|
||||
|
||||
const name = product.nameFa || product.title;
|
||||
const variant = variants.find((item) => item.id === variantId) ?? variants[0];
|
||||
const price = variant ? (variant.discountedPrice ?? variant.price) : (product.store?.minPrice ?? 0);
|
||||
const oldPrice = variant?.discountedPrice ? variant.price : undefined;
|
||||
const price = variant ? (variant.discountedPrice ?? variant.price ?? 0) : (product.store?.minPrice ?? 0);
|
||||
const oldPrice = variant?.discountedPrice ? (variant.price ?? undefined) : undefined;
|
||||
const stock = variant ? (variant.stockQuantity ?? 0) : product.store?.inStock ? 1 : 0;
|
||||
const unavailable = !price || stock === 0;
|
||||
const off = discountPercent(price, oldPrice);
|
||||
@@ -45,11 +46,11 @@ export function ProductView({ product, variants }: Props) {
|
||||
{off}%
|
||||
</span>
|
||||
)}
|
||||
<Media src={image} alt={product.nameFa || product.title} className="aspect-square" sizes="560px" priority />
|
||||
<Media src={image} alt={name} className="aspect-square" sizes="560px" priority />
|
||||
</div>
|
||||
{images.length > 1 && (
|
||||
<div className="mt-3 grid grid-cols-6 gap-2">
|
||||
{images.map((src) => (
|
||||
{images.map((src, index) => (
|
||||
<button
|
||||
key={src}
|
||||
type="button"
|
||||
@@ -58,7 +59,7 @@ export function ProductView({ product, variants }: Props) {
|
||||
src === image ? "border-brand" : "border-black/10"
|
||||
}`}
|
||||
>
|
||||
<Media src={src} alt="" className="size-full" sizes="80px" />
|
||||
<Media src={src} alt={`${name} — تصویر ${(index + 1).toLocaleString("fa-IR")}`} className="size-full" sizes="80px" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -67,7 +68,7 @@ export function ProductView({ product, variants }: Props) {
|
||||
|
||||
<div className="rounded-2xl border border-black/5 p-5 md:p-6 lg:col-span-6">
|
||||
{product.categoryNameFa && <p className="mb-2 text-xs text-zinc-400">{product.categoryNameFa}</p>}
|
||||
<h1 className="text-xl md:text-2xl font-extrabold leading-9">{product.nameFa || product.title}</h1>
|
||||
<p className="text-xl md:text-2xl font-extrabold leading-9">{name}</p>
|
||||
{product.title && product.nameFa && (
|
||||
<p dir="ltr" className="mt-1 text-sm text-zinc-400">
|
||||
{product.title}
|
||||
|
||||
@@ -100,11 +100,11 @@ export function SearchBox({ className = "", onNavigate }: Props) {
|
||||
return (
|
||||
<li key={product.id}>
|
||||
<Link
|
||||
href={product.slug ? `/products/${product.slug}` : `/products?q=${encodeURIComponent(query)}`}
|
||||
href={product.href || `/products?q=${encodeURIComponent(query)}`}
|
||||
onClick={onNavigate}
|
||||
className="flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-50"
|
||||
>
|
||||
<Media src={product.image} alt="" className="size-12 shrink-0 rounded-lg" sizes="48px" />
|
||||
<Media src={product.image} alt={product.name} className="size-12 shrink-0 rounded-lg" sizes="48px" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[13px] leading-6">{product.name}</p>
|
||||
{unavailable ? (
|
||||
|
||||
Reference in New Issue
Block a user