Add about/contact pages, real social icons, and gallery scroll fix

- Add /about (business-info driven, falls back to existing copy) and
  /contact (business-info contact details + a working form posting to
  the contact-submissions API)
- Point header/footer "About"/"Contact" links at the new pages instead
  of homepage anchors
- Replace the hand-drawn social icons with the real brand SVGs from
  public/icons (Instagram, Telegram, WhatsApp, Bale, Eitaa), inlined
  with currentColor so hover states work; shared via a single
  socialLinks list used by both header and footer
- Remove the horizontal scrollbar under the product gallery thumbnails
  by wrapping them instead of scrolling
This commit is contained in:
amirhosein.ashourloo
2026-08-24 15:06:35 +03:30
parent f10af62573
commit aba78b76a1
16 changed files with 479 additions and 82 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16.159" height="12.176" viewBox="0 0 16.159 12.176">
<path id="Path_19534" data-name="Path 19534" d="M12.119.553a1.783,1.783,0,0,0-2.583,0L5.554,4.536a1.783,1.783,0,0,0,0,2.583l8.486,8.487a1.826,1.826,0,1,0,2.583-2.583l-7.2-7.2,2.691-2.691A1.783,1.783,0,0,0,12.119.553Z" transform="translate(0 17.176) rotate(-90)" fill="#333132"/>
</svg>

After

Width:  |  Height:  |  Size: 388 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17.129" height="17.253" viewBox="0 0 17.129 17.253">
<g id="Group_18956" data-name="Group 18956" transform="translate(-234.629 -321.162)">
<path id="Path_19148" data-name="Path 19148" d="M240.486,335.387c-1.7-.823-2-2.692-.833-4.651-1.033-1.49-.66-3.185.845-3.579,0,.15,0,.286,0,.421a1.166,1.166,0,0,0,1.051,1.364,5.687,5.687,0,0,0,1.982,0,15.547,15.547,0,0,0,2.452-.849,1.989,1.989,0,0,0,.646-.507.674.674,0,0,0,.054-1.014A3.4,3.4,0,0,0,243.5,325.2a7.943,7.943,0,0,0-5.707,2.9,3.6,3.6,0,0,0-.866,1.828,2.775,2.775,0,0,0,.988,2.571.9.9,0,0,1,.329.709,3.457,3.457,0,0,0,2.49,2.99.615.615,0,0,0,.148,0c.135-.582.26-1.143.4-1.7.079-.321.244-.423.605-.331a4.768,4.768,0,0,0,4.037-.712,27.252,27.252,0,0,0,2.734-2.289c.759-.7,1.443-1.476,2.175-2.2a4.509,4.509,0,0,1,.871-.652c.018.694.241,1.332-.4,1.956a20.882,20.882,0,0,0-1.995,2.53,21.226,21.226,0,0,1-3.368,3.818,7.278,7.278,0,0,1-8.226.939,5.578,5.578,0,0,1-3.013-4.384c-.628-4.348,2.591-10,7.24-11.651a5.354,5.354,0,0,1,3.508-.106,4.521,4.521,0,0,1,3.133,5.034,4.73,4.73,0,0,1-3.028,4.224c-.625.274-1.237.578-1.855.869C242.05,332.313,240.64,333.3,240.486,335.387Z" transform="translate(0)" fill="#333132"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg id="Group_3409" data-name="Group 3409" xmlns="http://www.w3.org/2000/svg" width="15.746" height="15.746" viewBox="0 0 15.746 15.746">
<path id="Path_2388" data-name="Path 2388" d="M11.4,0H4.345A4.35,4.35,0,0,0,0,4.345V11.4a4.35,4.35,0,0,0,4.345,4.345H11.4A4.35,4.35,0,0,0,15.746,11.4V4.345A4.35,4.35,0,0,0,11.4,0Zm2.949,11.4A2.952,2.952,0,0,1,11.4,14.349H4.345A2.952,2.952,0,0,1,1.4,11.4V4.345A2.952,2.952,0,0,1,4.345,1.4H11.4a2.952,2.952,0,0,1,2.948,2.948V11.4Z" transform="translate(0)" fill="#333132"/>
<path id="Path_2389" data-name="Path 2389" d="M45.025,40.97a4.057,4.057,0,1,0,4.057,4.057A4.062,4.062,0,0,0,45.025,40.97Zm0,6.718a2.66,2.66,0,1,1,2.66-2.66A2.663,2.663,0,0,1,45.025,47.688Z" transform="translate(-37.152 -37.154)" fill="#333132"/>
<path id="Path_2390" data-name="Path 2390" d="M119.946,28.251a1.024,1.024,0,1,0,.725.3A1.029,1.029,0,0,0,119.946,28.251Z" transform="translate(-107.845 -25.62)" fill="#333132"/>
</svg>

After

Width:  |  Height:  |  Size: 948 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm121.8 169.9l-40.7 191.8c-3 13.6-11.1 16.9-22.4 10.5l-62-45.7-29.9 28.8c-3.3 3.3-6.1 6.1-12.5 6.1l4.4-63.1 114.9-103.8c5-4.4-1.1-6.9-7.7-2.5l-142 89.4-61.2-19.1c-13.3-4.2-13.6-13.3 2.8-19.7l239.1-92.2c11.1-4 20.8 2.7 17.2 19.5z"/></svg>

After

Width:  |  Height:  |  Size: 556 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M380.9 97.1C339 55.1 283.2 32 223.9 32c-122.4 0-222 99.6-222 222 0 39.1 10.2 77.3 29.6 111L0 480l117.7-30.9c32.4 17.7 68.9 27 106.1 27h.1c122.3 0 224.1-99.6 224.1-222 0-59.3-25.2-115-67.1-157zm-157 341.6c-33.2 0-65.7-8.9-94-25.7l-6.7-4-69.8 18.3L72 359.2l-4.4-7c-18.5-29.4-28.2-63.3-28.2-98.2 0-101.7 82.8-184.5 184.6-184.5 49.3 0 95.6 19.2 130.4 54.1 34.8 34.9 56.2 81.2 56.1 130.5 0 101.8-84.9 184.6-186.6 184.6zm101.2-138.2c-5.5-2.8-32.8-16.2-37.9-18-5.1-1.9-8.8-2.8-12.5 2.8-3.7 5.6-14.3 18-17.6 21.8-3.2 3.7-6.5 4.2-12 1.4-32.6-16.3-54-29.1-75.5-66-5.7-9.8 5.7-9.1 16.3-30.3 1.8-3.7.9-6.9-.5-9.7-1.4-2.8-12.5-30.1-17.1-41.2-4.5-10.8-9.1-9.3-12.5-9.5-3.2-.2-6.9-.2-10.6-.2-3.7 0-9.7 1.4-14.8 6.9-5.1 5.6-19.4 19-19.4 46.3 0 27.3 19.9 53.7 22.6 57.4 2.8 3.7 39.1 59.7 94.8 83.8 35.2 15.2 49 16.5 66.6 13.9 10.7-1.6 32.8-13.4 37.4-26.4 4.6-13 4.6-24.1 3.2-26.4-1.3-2.5-5-3.9-10.5-6.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+88
View File
@@ -0,0 +1,88 @@
import Link from "next/link";
import { getBusinessInfo, safe } from "@/lib/api";
import { ShieldIcon, TagIcon, TruckIcon } from "@/components/icons";
const FALLBACK_ABOUT =
"فروشگاه اینترنتی کودکانه، تولیدکننده و عرضه‌کننده پوشاک بچگانه با جدیدترین طرح‌ها برای نوزادان، دختران و پسران است. تمام محصولات با پارچه‌های نرم و مناسب پوست حساس کودکان تولید می‌شوند و پیش از انتشار در سایت، از نظر کیفیت دوخت و اصالت بررسی می‌شوند تا خیالتان برای انتخاب لباس فرزندتان راحت باشد.";
const points = [
{
icon: ShieldIcon,
title: "ضمانت اصالت کالا",
desc: "تمامی محصولات پیش از ارسال از نظر کیفیت و اصالت بررسی می‌شوند.",
},
{
icon: TruckIcon,
title: "ارسال سریع و رایگان",
desc: "ارسال به سراسر کشور با پیک و پست پیشتاز، رایگان برای خریدهای بالای سقف تعیین‌شده.",
},
{
icon: TagIcon,
title: "قیمت مناسب و عمده",
desc: "خرید مستقیم از تولیدی با قیمت رقابتی برای مشتریان خرد و عمده.",
},
];
export const metadata = {
title: "درباره ما | کودکانه",
};
export default async function AboutPage() {
const info = await safe(getBusinessInfo(), null);
const about = info?.about?.trim() || FALLBACK_ABOUT;
const vision = info?.vision?.trim();
return (
<main className="boxed py-8 md:py-12">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-14 items-center">
<div className="overflow-hidden rounded-lg border border-line">
<img
src="/images/about/about.svg"
alt=""
className="w-full aspect-[9/8] object-cover"
/>
</div>
<div>
<span className="font-en text-xs md:text-sm font-semibold tracking-[4px] text-gold-dark uppercase">
About Us
</span>
<h1 className="mt-1.5 text-xl md:text-3xl font-extrabold text-ink">درباره ما</h1>
<span className="mt-3 block h-[3px] w-14 bg-brand rounded-full" />
<p className="mt-5 text-sm md:text-[15px] leading-7 md:leading-8 text-body">{about}</p>
{vision && (
<>
<h2 className="mt-6 text-sm md:text-base font-bold text-ink">چشم‌انداز ما</h2>
<p className="mt-2 text-sm md:text-[15px] leading-7 md:leading-8 text-body">
{vision}
</p>
</>
)}
<div className="mt-7 space-y-5">
{points.map((p) => (
<div key={p.title} className="flex items-start gap-3.5">
<span className="flex items-center justify-center size-11 rounded-lg bg-brand-light text-brand shrink-0">
<p.icon className="size-5" />
</span>
<div>
<h3 className="text-sm md:text-[15px] font-bold text-ink">{p.title}</h3>
<p className="mt-1 text-xs md:text-sm text-body leading-6">{p.desc}</p>
</div>
</div>
))}
</div>
<Link
href="/contact"
className="mt-8 inline-flex items-center rounded-md bg-brand px-6 py-3 text-sm font-bold text-white hover:bg-brand-dark transition-colors"
>
تماس با ما
</Link>
</div>
</div>
</main>
);
}
+92
View File
@@ -0,0 +1,92 @@
import { getBusinessInfo, safe } from "@/lib/api";
import { siteInfo } from "@/data/site";
import { socialLinks } from "@/components/socialLinks";
import ContactForm from "@/components/ContactForm";
import { MailIcon, PhoneIcon, PinIcon } from "@/components/icons";
export const metadata = {
title: "تماس با ما | کودکانه",
};
// business-info's emails/phoneNumbers/addresses shape isn't documented —
// read either a plain string or a {value|number|email|address|...} object.
function toText(item: unknown): string {
if (typeof item === "string") return item;
if (item && typeof item === "object") {
const obj = item as Record<string, unknown>;
const val = obj.value ?? obj.number ?? obj.email ?? obj.address ?? obj.text ?? obj.label;
if (typeof val === "string") return val;
}
return "";
}
export default async function ContactPage() {
const info = await safe(getBusinessInfo(), null);
const phone = info?.phoneNumbers.map(toText).find(Boolean) || siteInfo.phone;
const email = info?.emails.map(toText).find(Boolean) || siteInfo.email;
const address = info?.addresses.map(toText).find(Boolean) || siteInfo.address;
return (
<main className="boxed py-8 md:py-12">
<div className="mb-8 md:mb-10">
<span className="font-en text-xs md:text-sm font-semibold tracking-[4px] text-gold-dark uppercase">
Contact Us
</span>
<h1 className="mt-1.5 text-xl md:text-3xl font-extrabold text-ink">تماس با ما</h1>
<span className="mt-3 block h-[3px] w-14 bg-brand rounded-full" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-[320px_1fr] gap-8 lg:gap-12">
<div className="space-y-5">
<div className="rounded-xl border border-line p-5">
<ul className="space-y-4 text-sm">
<li className="flex items-start gap-3">
<span className="flex items-center justify-center size-9 rounded-lg bg-brand-light text-brand shrink-0">
<PhoneIcon className="size-4.5" />
</span>
<span className="pt-1.5" dir="ltr">
{phone}
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex items-center justify-center size-9 rounded-lg bg-brand-light text-brand shrink-0">
<MailIcon className="size-4.5" />
</span>
<span className="pt-1.5" dir="ltr">
{email}
</span>
</li>
<li className="flex items-start gap-3">
<span className="flex items-center justify-center size-9 rounded-lg bg-brand-light text-brand shrink-0">
<PinIcon className="size-4.5" />
</span>
<span className="pt-1.5">{address}</span>
</li>
</ul>
</div>
<div className="rounded-xl border border-line p-5">
<p className="text-xs font-bold text-ink mb-3">ما را دنبال کنید</p>
<div className="flex items-center gap-2.5">
{socialLinks.map(({ icon: Icon, label, href }) => (
<a
key={label}
href={href}
aria-label={label}
className="flex items-center justify-center size-9 rounded-full border border-line text-ink transition-all duration-200 hover:border-brand hover:bg-brand hover:text-white hover:scale-110"
>
<Icon className="size-4" />
</a>
))}
</div>
</div>
</div>
<div className="rounded-xl border border-line p-5 md:p-8">
<ContactForm />
</div>
</div>
</main>
);
}
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { useState, type FormEvent } from "react";
import { submitContactForm } from "@/lib/api";
import { CheckIcon } from "./icons";
type Status = "idle" | "loading" | "success" | "error";
const inputClass =
"w-full rounded-lg border border-line bg-surface px-4 h-11 text-sm outline-none focus:border-brand transition-colors";
export default function ContactForm() {
const [status, setStatus] = useState<Status>("idle");
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const data = new FormData(form);
const name = String(data.get("name") || "").trim();
const title = String(data.get("title") || "").trim();
const text = String(data.get("text") || "").trim();
const email = String(data.get("email") || "").trim();
const cellNumber = String(data.get("cellNumber") || "").trim();
if (!name || !title || !text) return;
setStatus("loading");
try {
await submitContactForm({
name,
title,
text,
email: email || undefined,
cellNumber: cellNumber || undefined,
});
setStatus("success");
form.reset();
} catch {
setStatus("error");
}
}
if (status === "success") {
return (
<div className="rounded-xl border border-brand/30 bg-brand-light p-6 text-center">
<span className="mx-auto flex items-center justify-center size-11 rounded-full bg-brand text-white mb-3">
<CheckIcon className="size-5" />
</span>
<p className="text-sm font-bold text-brand">پیام شما با موفقیت ارسال شد.</p>
<p className="mt-1 text-xs text-body">به‌زودی با شما تماس می‌گیریم.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label htmlFor="name" className="block text-xs font-bold text-ink mb-1.5">
نام و نام خانوادگی *
</label>
<input id="name" name="name" required className={inputClass} />
</div>
<div>
<label htmlFor="title" className="block text-xs font-bold text-ink mb-1.5">
موضوع *
</label>
<input id="title" name="title" required className={inputClass} />
</div>
<div>
<label htmlFor="email" className="block text-xs font-bold text-ink mb-1.5">
ایمیل
</label>
<input id="email" name="email" type="email" dir="ltr" className={inputClass} />
</div>
<div>
<label htmlFor="cellNumber" className="block text-xs font-bold text-ink mb-1.5">
شماره تماس
</label>
<input id="cellNumber" name="cellNumber" type="tel" dir="ltr" className={inputClass} />
</div>
</div>
<div>
<label htmlFor="text" className="block text-xs font-bold text-ink mb-1.5">
متن پیام *
</label>
<textarea
id="text"
name="text"
required
rows={5}
className={`${inputClass} h-auto py-3 resize-none`}
/>
</div>
{status === "error" && (
<p className="text-xs text-red-600">ارسال پیام با خطا مواجه شد. لطفاً دوباره تلاش کنید.</p>
)}
<button
type="submit"
disabled={status === "loading"}
className="w-full sm:w-auto flex items-center justify-center rounded-md bg-brand px-8 h-12 text-sm font-bold text-white hover:bg-brand-dark transition-colors disabled:opacity-60"
>
{status === "loading" ? "در حال ارسال..." : "ارسال پیام"}
</button>
</form>
);
}
+21 -32
View File
@@ -3,14 +3,8 @@ import Logo from "./Logo";
import { headerNavLinks, siteInfo } from "@/data/site";
import { categoryHref } from "@/lib/slug";
import type { CategoryNode } from "@/lib/types";
import {
InstagramIcon,
MailIcon,
PhoneIcon,
PinIcon,
TelegramIcon,
WhatsappIcon,
} from "./icons";
import { socialLinks } from "./socialLinks";
import { MailIcon, PhoneIcon, PinIcon } from "./icons";
export default function Footer({ categoryColumns }: { categoryColumns: CategoryNode[] }) {
const categoryItems = categoryColumns.flatMap((col) => col.children).slice(0, 6);
@@ -25,27 +19,16 @@ export default function Footer({ categoryColumns }: { categoryColumns: CategoryN
دختران و پسران با ارسال به سراسر کشور.
</p>
<div className="mt-5 flex items-center gap-3">
<a
href="#"
aria-label="اینستاگرام"
className="flex items-center justify-center size-9 rounded-full border border-white/15 hover:border-gold hover:text-gold transition-colors"
>
<InstagramIcon className="size-4" />
</a>
<a
href="#"
aria-label="تلگرام"
className="flex items-center justify-center size-9 rounded-full border border-white/15 hover:border-gold hover:text-gold transition-colors"
>
<TelegramIcon className="size-4" />
</a>
<a
href="#"
aria-label="واتساپ"
className="flex items-center justify-center size-9 rounded-full border border-white/15 hover:border-gold hover:text-gold transition-colors"
>
<WhatsappIcon className="size-4" />
</a>
{socialLinks.map(({ icon: Icon, label, href }) => (
<a
key={label}
href={href}
aria-label={label}
className="flex items-center justify-center size-9 rounded-full border border-white/15 transition-all duration-200 hover:border-gold hover:bg-gold hover:text-ink hover:scale-110"
>
<Icon className="size-4" />
</a>
))}
</div>
</div>
@@ -54,9 +37,15 @@ export default function Footer({ categoryColumns }: { categoryColumns: CategoryN
<ul className="space-y-2.5 text-sm">
{headerNavLinks.map((link) => (
<li key={link.title}>
<a href={link.href} className="hover:text-gold transition-colors">
{link.title}
</a>
{link.href.startsWith("#") ? (
<a href={link.href} className="hover:text-gold transition-colors">
{link.title}
</a>
) : (
<Link href={link.href} className="hover:text-gold transition-colors">
{link.title}
</Link>
)}
</li>
))}
</ul>
+47 -39
View File
@@ -4,26 +4,13 @@ import { useState } from "react";
import Link from "next/link";
import Logo from "./Logo";
import SearchBox from "./SearchBox";
import {
BagIcon,
ChevronDownIcon,
CloseIcon,
InstagramIcon,
MenuIcon,
TelegramIcon,
WhatsappIcon,
} from "./icons";
import { BagIcon, ChevronDownIcon, CloseIcon, MenuIcon } from "./icons";
import { socialLinks } from "./socialLinks";
import { headerNavLinks } from "@/data/site";
import { CUSTOMER_PORTAL_URL } from "@/lib/config";
import { categoryHref } from "@/lib/slug";
import type { CategoryNode } from "@/lib/types";
const socialLinks = [
{ icon: InstagramIcon, label: "اینستاگرام" },
{ icon: TelegramIcon, label: "تلگرام" },
{ icon: WhatsappIcon, label: "واتساپ" },
];
export default function Header({ categoryColumns }: { categoryColumns: CategoryNode[] }) {
const [productsOpen, setProductsOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
@@ -115,24 +102,34 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
)}
</div>
{headerNavLinks.map((link) => (
<a
key={link.title}
href={link.href}
className="flex h-full items-center px-4 text-sm font-medium text-white/90 hover:text-gold transition-colors"
>
{link.title}
</a>
))}
{headerNavLinks.map((link) =>
link.href.startsWith("#") ? (
<a
key={link.title}
href={link.href}
className="flex h-full items-center px-4 text-sm font-medium text-white/90 hover:text-gold transition-colors"
>
{link.title}
</a>
) : (
<Link
key={link.title}
href={link.href}
className="flex h-full items-center px-4 text-sm font-medium text-white/90 hover:text-gold transition-colors"
>
{link.title}
</Link>
)
)}
</nav>
<div className="flex items-center gap-2">
{socialLinks.map(({ icon: Icon, label }) => (
{socialLinks.map(({ icon: Icon, label, href }) => (
<a
key={label}
href="#"
href={href}
aria-label={label}
className="flex items-center justify-center size-8 rounded-md bg-white/10 text-white hover:bg-white/20 transition-colors"
className="flex items-center justify-center size-8 rounded-md bg-white/10 text-white transition-all duration-200 hover:bg-gold hover:text-ink hover:scale-110"
>
<Icon className="size-4" />
</a>
@@ -207,15 +204,26 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
</div>
)}
{headerNavLinks.map((link) => (
<a
key={link.title}
href={link.href}
className="block py-3 text-sm font-bold text-ink border-b border-line"
>
{link.title}
</a>
))}
{headerNavLinks.map((link) =>
link.href.startsWith("#") ? (
<a
key={link.title}
href={link.href}
className="block py-3 text-sm font-bold text-ink border-b border-line"
>
{link.title}
</a>
) : (
<Link
key={link.title}
href={link.href}
onClick={() => setMobileOpen(false)}
className="block py-3 text-sm font-bold text-ink border-b border-line"
>
{link.title}
</Link>
)
)}
<a
href={CUSTOMER_PORTAL_URL}
@@ -225,12 +233,12 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
</a>
<div className="mt-5 flex items-center justify-center gap-2">
{socialLinks.map(({ icon: Icon, label }) => (
{socialLinks.map(({ icon: Icon, label, href }) => (
<a
key={label}
href="#"
href={href}
aria-label={label}
className="flex items-center justify-center size-9 rounded-md border border-line text-ink"
className="flex items-center justify-center size-9 rounded-md border border-line text-ink transition-all duration-200 hover:border-brand hover:bg-brand hover:text-white hover:scale-110"
>
<Icon className="size-4" />
</a>
+1 -1
View File
@@ -42,7 +42,7 @@ export default function ProductGallery({ images, title }: { images: string[]; ti
</div>
{gallery.length > 1 && (
<div className="mt-3 flex gap-2.5 overflow-x-auto">
<div className="mt-3 flex flex-wrap gap-2.5">
{gallery.map((src, i) => (
<button
key={src + i}
+36 -8
View File
@@ -105,26 +105,54 @@ export function TagIcon({ className }: IconProps) {
export function InstagramIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={className} {...base}>
<rect x="3.5" y="3.5" width="17" height="17" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17" cy="7" r="0.6" fill="currentColor" stroke="none" />
<svg viewBox="0 0 15.746 15.746" className={className} fill="currentColor" stroke="none">
<path d="M11.4,0H4.345A4.35,4.35,0,0,0,0,4.345V11.4a4.35,4.35,0,0,0,4.345,4.345H11.4A4.35,4.35,0,0,0,15.746,11.4V4.345A4.35,4.35,0,0,0,11.4,0Zm2.949,11.4A2.952,2.952,0,0,1,11.4,14.349H4.345A2.952,2.952,0,0,1,1.4,11.4V4.345A2.952,2.952,0,0,1,4.345,1.4H11.4a2.952,2.952,0,0,1,2.948,2.948V11.4Z" />
<path
d="M45.025,40.97a4.057,4.057,0,1,0,4.057,4.057A4.062,4.062,0,0,0,45.025,40.97Zm0,6.718a2.66,2.66,0,1,1,2.66-2.66A2.663,2.663,0,0,1,45.025,47.688Z"
transform="translate(-37.152 -37.154)"
/>
<path
d="M119.946,28.251a1.024,1.024,0,1,0,.725.3A1.029,1.029,0,0,0,119.946,28.251Z"
transform="translate(-107.845 -25.62)"
/>
</svg>
);
}
export function TelegramIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={className} fill="currentColor" stroke="none">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.894 8.221-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.446 1.394c-.16.16-.295.295-.605.295l.213-3.053 5.56-5.023c.242-.213-.054-.334-.373-.121l-6.869 4.326-2.96-.924c-.643-.204-.658-.643.136-.953l11.57-4.461c.538-.196 1.006.128.828.941z" />
<svg viewBox="0 0 496 512" className={className} fill="currentColor" stroke="none">
<path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm121.8 169.9l-40.7 191.8c-3 13.6-11.1 16.9-22.4 10.5l-62-45.7-29.9 28.8c-3.3 3.3-6.1 6.1-12.5 6.1l4.4-63.1 114.9-103.8c5-4.4-1.1-6.9-7.7-2.5l-142 89.4-61.2-19.1c-13.3-4.2-13.6-13.3 2.8-19.7l239.1-92.2c11.1-4 20.8 2.7 17.2 19.5z" />
</svg>
);
}
export function WhatsappIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={className} fill="currentColor" stroke="none">
<path d="M12.04 2.003c-5.5 0-9.96 4.46-9.96 9.96 0 1.76.46 3.48 1.34 4.99L2 22l5.2-1.36a9.94 9.94 0 0 0 4.84 1.24c5.5 0 9.96-4.46 9.96-9.96 0-2.66-1.04-5.16-2.92-7.04a9.9 9.9 0 0 0-7.04-2.92zm0 1.8c2.15 0 4.17.84 5.7 2.36a8.03 8.03 0 0 1 2.36 5.7c0 4.44-3.62 8.06-8.07 8.06a8.05 8.05 0 0 1-4.1-1.12l-.29-.17-3.06.8.82-2.99-.19-.31a7.98 7.98 0 0 1-1.23-4.28c0-4.45 3.61-8.06 8.06-8.06zm-4.4 4.6c-.15 0-.39.05-.6.29-.2.23-.78.76-.78 1.85s.8 2.15.91 2.3c.11.15 1.56 2.47 3.83 3.37.53.22.95.35 1.28.44.54.15 1.03.13 1.42.08.43-.07 1.34-.55 1.53-1.08.19-.53.19-.99.13-1.08-.06-.1-.2-.15-.43-.27-.22-.11-1.34-.66-1.55-.74-.21-.08-.36-.11-.51.11-.15.22-.58.74-.71.89-.13.15-.26.16-.48.05-.22-.11-.95-.35-1.81-1.12-.67-.6-1.12-1.33-1.25-1.55-.13-.22-.01-.34.1-.45.1-.1.22-.26.33-.39.11-.13.15-.22.22-.37.07-.15.04-.28-.02-.39-.05-.11-.51-1.27-.71-1.74-.19-.45-.38-.39-.51-.4h-.44z" />
<svg viewBox="0 0 448 512" className={className} fill="currentColor" stroke="none">
<path d="M380.9 97.1C339 55.1 283.2 32 223.9 32c-122.4 0-222 99.6-222 222 0 39.1 10.2 77.3 29.6 111L0 480l117.7-30.9c32.4 17.7 68.9 27 106.1 27h.1c122.3 0 224.1-99.6 224.1-222 0-59.3-25.2-115-67.1-157zm-157 341.6c-33.2 0-65.7-8.9-94-25.7l-6.7-4-69.8 18.3L72 359.2l-4.4-7c-18.5-29.4-28.2-63.3-28.2-98.2 0-101.7 82.8-184.5 184.6-184.5 49.3 0 95.6 19.2 130.4 54.1 34.8 34.9 56.2 81.2 56.1 130.5 0 101.8-84.9 184.6-186.6 184.6zm101.2-138.2c-5.5-2.8-32.8-16.2-37.9-18-5.1-1.9-8.8-2.8-12.5 2.8-3.7 5.6-14.3 18-17.6 21.8-3.2 3.7-6.5 4.2-12 1.4-32.6-16.3-54-29.1-75.5-66-5.7-9.8 5.7-9.1 16.3-30.3 1.8-3.7.9-6.9-.5-9.7-1.4-2.8-12.5-30.1-17.1-41.2-4.5-10.8-9.1-9.3-12.5-9.5-3.2-.2-6.9-.2-10.6-.2-3.7 0-9.7 1.4-14.8 6.9-5.1 5.6-19.4 19-19.4 46.3 0 27.3 19.9 53.7 22.6 57.4 2.8 3.7 39.1 59.7 94.8 83.8 35.2 15.2 49 16.5 66.6 13.9 10.7-1.6 32.8-13.4 37.4-26.4 4.6-13 4.6-24.1 3.2-26.4-1.3-2.5-5-3.9-10.5-6.6z" />
</svg>
);
}
export function BaleIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 16.159 12.176" className={className} fill="currentColor" stroke="none">
<path
d="M12.119.553a1.783,1.783,0,0,0-2.583,0L5.554,4.536a1.783,1.783,0,0,0,0,2.583l8.486,8.487a1.826,1.826,0,1,0,2.583-2.583l-7.2-7.2,2.691-2.691A1.783,1.783,0,0,0,12.119.553Z"
transform="translate(0 17.176) rotate(-90)"
/>
</svg>
);
}
export function EitaaIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 17.129 17.253" className={className} fill="currentColor" stroke="none">
<path
d="M240.486,335.387c-1.7-.823-2-2.692-.833-4.651-1.033-1.49-.66-3.185.845-3.579,0,.15,0,.286,0,.421a1.166,1.166,0,0,0,1.051,1.364,5.687,5.687,0,0,0,1.982,0,15.547,15.547,0,0,0,2.452-.849,1.989,1.989,0,0,0,.646-.507.674.674,0,0,0,.054-1.014A3.4,3.4,0,0,0,243.5,325.2a7.943,7.943,0,0,0-5.707,2.9,3.6,3.6,0,0,0-.866,1.828,2.775,2.775,0,0,0,.988,2.571.9.9,0,0,1,.329.709,3.457,3.457,0,0,0,2.49,2.99.615.615,0,0,0,.148,0c.135-.582.26-1.143.4-1.7.079-.321.244-.423.605-.331a4.768,4.768,0,0,0,4.037-.712,27.252,27.252,0,0,0,2.734-2.289c.759-.7,1.443-1.476,2.175-2.2a4.509,4.509,0,0,1,.871-.652c.018.694.241,1.332-.4,1.956a20.882,20.882,0,0,0-1.995,2.53,21.226,21.226,0,0,1-3.368,3.818,7.278,7.278,0,0,1-8.226.939,5.578,5.578,0,0,1-3.013-4.384c-.628-4.348,2.591-10,7.24-11.651a5.354,5.354,0,0,1,3.508-.106,4.521,4.521,0,0,1,3.133,5.034,4.73,4.73,0,0,1-3.028,4.224c-.625.274-1.237.578-1.855.869C242.05,332.313,240.64,333.3,240.486,335.387Z"
transform="translate(-234.629 -321.162)"
/>
</svg>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { BaleIcon, EitaaIcon, InstagramIcon, TelegramIcon, WhatsappIcon } from "./icons";
export const socialLinks = [
{ icon: InstagramIcon, label: "اینستاگرام", href: "#" },
{ icon: TelegramIcon, label: "تلگرام", href: "#" },
{ icon: WhatsappIcon, label: "واتساپ", href: "#" },
{ icon: BaleIcon, label: "بله", href: "#" },
{ icon: EitaaIcon, label: "ایتا", href: "#" },
];
+2 -2
View File
@@ -8,7 +8,7 @@ export const siteInfo = {
export const headerNavLinks = [
{ title: "تخفیف‌های ویژه", href: "#special-sale" },
{ title: "درباره ما", href: "#about" },
{ title: "درباره ما", href: "/about" },
{ title: "سوالات متداول", href: "#faq" },
{ title: "تماس با ما", href: "#contact" },
{ title: "تماس با ما", href: "/contact" },
];
+24
View File
@@ -3,6 +3,8 @@ import type {
ApiBlog,
ApiCategory,
ApiProduct,
BusinessInfo,
ContactFormInput,
EntityComment,
PagedResult,
ProductVariation,
@@ -31,6 +33,18 @@ async function apiGet<T>(path: string, params?: QueryParams): Promise<T> {
return res.json() as Promise<T>;
}
async function apiPost<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`API request failed (${res.status}): ${path}`);
}
return res.json() as Promise<T>;
}
// Swallows errors so a backend hiccup degrades a section instead of
// crashing the whole page.
export async function safe<T>(promise: Promise<T>, fallback: T): Promise<T> {
@@ -125,6 +139,16 @@ export function getStoreSpecials(): Promise<StoreSpecial[]> {
);
}
export function getBusinessInfo(): Promise<BusinessInfo> {
return apiGet(`/tenants/${SITE_DOMAIN}/website/business-info`);
}
export function submitContactForm(
data: ContactFormInput
): Promise<{ submission: unknown; message: string }> {
return apiPost(`/tenants/${SITE_DOMAIN}/contact-submissions`, data);
}
export function findSlot(
slots: StaticImageSlot[],
key: string
+33
View File
@@ -1,3 +1,36 @@
export interface SocialMediaLinks {
whatsapp?: string;
telegram?: string;
instagram?: string;
linkedin?: string;
youtube?: string;
aparat?: string;
}
// emails/phoneNumbers/addresses shapes aren't documented and this tenant
// has none set yet — kept loose and read defensively at render time.
export interface BusinessInfo {
id: string;
name: string;
nameFa: string;
about: string;
vision: string;
logoUrl: string | null;
faviconUrl: string | null;
emails: unknown[];
phoneNumbers: unknown[];
socialMedia: SocialMediaLinks;
addresses: unknown[];
}
export interface ContactFormInput {
title: string;
name: string;
text: string;
email?: string;
cellNumber?: string;
}
export interface StaticImageItem {
id: string;
url: string;