first commit

This commit is contained in:
amirhosein.ashourloo
2026-09-02 17:03:23 +03:30
commit 1bd131e9d8
91 changed files with 11211 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
import Image from "next/image";
import Link from "next/link";
import type { CSSProperties } from "react";
import { toCssAspectRatio } from "@/lib/format";
import type { StaticImageSlot } from "@/lib/types";
// Shared renderer for the "one-banner-bg" / "two-banner-bg" / "three-banner-bg"
// static-image slots: url -> <img src>, linkUrl -> wrap in <a>, slot.aspectRatio
// drives the frame. An empty or failed slot keeps a flat, labelled placeholder
// (this site's usual stand-in for undesigned art, see
// public/images/categories/{id}.svg) instead of a fake photo, sized so the
// business/designer can see exactly what to fill in.
function BannerGrid({
slot,
count,
defaultAspect,
recommendedWidth,
cols,
}: {
slot?: StaticImageSlot;
count: number;
defaultAspect: string;
recommendedWidth: number;
cols: string;
}) {
const images = (slot?.images ?? []).slice(0, count);
const cssAspect = toCssAspectRatio(slot?.aspectRatio) ?? defaultAspect;
const aspectStyle = { aspectRatio: cssAspect } as CSSProperties;
const cells = Array.from({ length: count }, (_, i) => images[i]);
return (
<section className={`boxed mt-10`} aria-label="بنرهای تبلیغاتی">
<div className={`grid gap-3 ${cols}`}>
{cells.map((img, i) => {
if (!img) {
return (
<div
key={i}
className="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-line bg-surface text-body"
style={aspectStyle}
>
<span className="text-[12px]">بنر تبلیغاتی</span>
<span className="text-[11px] opacity-70" dir="ltr">
{recommendedWidth}px · {cssAspect.replace("/", ":")}
</span>
</div>
);
}
const media = (
<Image
src={img.url}
alt={img.titleFa || img.titleEn || "بنر تبلیغاتی"}
fill
sizes={`(max-width: 768px) 100vw, ${recommendedWidth}px`}
className="object-cover"
/>
);
const frameClass = "relative block overflow-hidden rounded-xl bg-surface";
// Only wrap in <a> when the slot actually carries a link — an
// undesignated banner shouldn't become a dead click target.
return img.linkUrl ? (
<Link key={img.id} href={img.linkUrl} className={frameClass} style={aspectStyle}>
{media}
</Link>
) : (
<div key={img.id} className={frameClass} style={aspectStyle}>
{media}
</div>
);
})}
</div>
</section>
);
}
export function OneBanner({ slot }: { slot?: StaticImageSlot }) {
return <BannerGrid slot={slot} count={1} defaultAspect="16/5" recommendedWidth={1200} cols="grid-cols-1" />;
}
export function TwoBanners({ slot }: { slot?: StaticImageSlot }) {
return (
<BannerGrid slot={slot} count={2} defaultAspect="16/9" recommendedWidth={580} cols="grid-cols-1 md:grid-cols-2" />
);
}
export function ThreeBanners({ slot }: { slot?: StaticImageSlot }) {
return (
<BannerGrid slot={slot} count={3} defaultAspect="4/3" recommendedWidth={380} cols="grid-cols-1 sm:grid-cols-3" />
);
}