Wire Meshkee static images, SEO, and customer account menu.

Connect homepage image slots and favicon to the API, add sitemap config and page metadata, and implement header login with profile/admin dropdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-01 11:22:02 +03:30
co-authored by Cursor
parent 424e339986
commit 7e8a1ef7dc
33 changed files with 1708 additions and 150 deletions
+25 -7
View File
@@ -1,6 +1,16 @@
import SlotImage from "@/components/SlotImage";
import { aboutText } from "@/data/home";
import type { SlotOverlay } from "@/lib/static-images";
import { parseAspectRatio } from "@/lib/static-images";
type AboutSectionProps = {
banner: SlotOverlay;
};
export default function AboutSection({ banner }: AboutSectionProps) {
const bannerImage = banner.items[0];
const ratio = parseAspectRatio(banner.aspectRatio);
export default function AboutSection() {
return (
<div className="wb-frame" id="about-section">
<section
@@ -60,12 +70,20 @@ export default function AboutSection() {
</div>
<div className="col-lg-6 order-force-1">
<div className="bg-img text-center">
<img
className="mt-5 mr-5"
style={{ height: "450px" }}
alt="درباره سیف طب"
src="/images/sections/about-bg.png"
/>
{bannerImage ? (
<SlotImage
className="mt-5 mr-5"
style={{
height: ratio ? undefined : "450px",
width: "auto",
maxWidth: "100%",
...(ratio ? { aspectRatio: ratio, height: "auto", maxHeight: "450px" } : {}),
}}
src={bannerImage.image}
alt={bannerImage.title || "درباره سیف طب"}
fallbackSrc={bannerImage.fallbackSrc}
/>
) : null}
</div>
</div>
</div>
+139
View File
@@ -0,0 +1,139 @@
"use client";
import { useEffect, useRef, useState, type MouseEvent } from "react";
import { businessDashboardUrl, customerPortalUrl } from "@/lib/config";
import { useSession, userDisplayName } from "@/lib/session";
function UserIcon() {
return <i className="WMi-user-linear btn-cart" aria-hidden />;
}
function ChevronIcon({ open }: { open: boolean }) {
return (
<i
className={`fa fa-angle-down account-menu__chevron${open ? " open" : ""}`}
aria-hidden
style={{ lineHeight: 0, fontSize: "12px" }}
/>
);
}
export default function AccountMenu() {
const { user, isAuthenticated, ready, isWebsiteAdmin, logout, getAccessToken } = useSession();
const [open, setOpen] = useState(false);
const [handoffBusy, setHandoffBusy] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onClickOutside = (event: Event) => {
if (menuRef.current && event.target instanceof Node && !menuRef.current.contains(event.target)) {
setOpen(false);
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onClickOutside);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("mousedown", onClickOutside);
document.removeEventListener("keydown", onKeyDown);
};
}, [open]);
async function openBusinessDashboard(event: MouseEvent<HTMLAnchorElement>) {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
return;
}
event.preventDefault();
if (handoffBusy) return;
const baseUrl = businessDashboardUrl;
const popup = window.open("about:blank", "_blank");
setHandoffBusy(true);
try {
const token = getAccessToken();
const res = await fetch("/api/auth/handoff", {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
if (!res.ok) throw new Error("handoff failed");
const data = (await res.json()) as { ticket?: string };
const url = new URL(baseUrl);
if (data.ticket) url.searchParams.set("handoff", data.ticket);
if (popup) {
popup.opener = null;
popup.location.replace(url.toString());
} else {
window.open(url.toString(), "_blank", "noopener,noreferrer");
}
} catch {
if (popup) popup.location.replace(baseUrl);
else window.open(baseUrl, "_blank", "noopener,noreferrer");
} finally {
setHandoffBusy(false);
setOpen(false);
}
}
if (!ready || !isAuthenticated || !user) {
return (
<a
href={customerPortalUrl}
className="account-menu account-menu--login d-inline-flex color-black align-items-center"
aria-label="ورود"
title="ورود"
>
<UserIcon />
</a>
);
}
const displayName = userDisplayName(user);
return (
<div ref={menuRef} className="account-menu account-menu--user">
<button
type="button"
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-haspopup="menu"
className="account-menu__trigger d-inline-flex color-black align-items-center"
>
<UserIcon />
<span className="account-menu__name">{displayName} عزیز</span>
<ChevronIcon open={open} />
</button>
{open ? (
<div role="menu" className="account-menu__dropdown">
<a
role="menuitem"
href={`${customerPortalUrl}/profile`}
onClick={() => setOpen(false)}
>
پروفایل من
</a>
{isWebsiteAdmin ? (
<a role="menuitem" href={businessDashboardUrl} onClick={openBusinessDashboard}>
{handoffBusy ? "در حال انتقال…" : "مدیریت وبسایت"}
</a>
) : null}
<button
type="button"
role="menuitem"
className="account-menu__logout"
onClick={() => {
setOpen(false);
logout();
}}
>
خروج
</button>
</div>
) : null}
</div>
);
}
+2 -2
View File
@@ -17,9 +17,9 @@ export default function Footer() {
<div className="col-md-6 col-lg-6">
<div className="widget-item mb-sm-30">
<div className="about-widget">
<h1 style={{ letterSpacing: "0px", fontWeight: 0, color: "#FFFFFF", fontSize: "20px" }}>
<h2 style={{ letterSpacing: "0px", fontWeight: 0, color: "#FFFFFF", fontSize: "20px" }}>
سیف طب
</h1>
</h2>
<p
style={{ fontWeight: 0, textAlign: "justify", letterSpacing: "0px" }}
dangerouslySetInnerHTML={{ __html: footerDescription }}
+2 -3
View File
@@ -1,5 +1,6 @@
import Link from "next/link";
import AccountMenu from "@/components/AccountMenu";
import { navLinks } from "@/data/home";
type SubMenuItem = {
@@ -91,9 +92,7 @@ export default function Header() {
</a>
</div>
<div className="d-none d-lg-block">
<a className="d-inline-flex color-black align-items-center" href="#">
<i className="WMi-user-linear btn-cart" />
</a>
<AccountMenu />
</div>
</div>
</div>
+76 -47
View File
@@ -1,6 +1,18 @@
import SlotImage from "@/components/SlotImage";
import SlotLink from "@/components/SlotLink";
import { quickLinks } from "@/data/home";
import type { SlotOverlay } from "@/lib/static-images";
import { parseAspectRatio } from "@/lib/static-images";
type HeroSectionProps = {
slider: SlotOverlay;
fourIcon: SlotOverlay;
twoBanner: SlotOverlay;
};
export default function HeroSection({ slider, fourIcon, twoBanner }: HeroSectionProps) {
const sliderRatio = parseAspectRatio(slider.aspectRatio);
export default function HeroSection() {
return (
<>
<div className="wb-frame" id="slider">
@@ -8,17 +20,22 @@ export default function HeroSection() {
<div className="home-slider-content">
<div className="swiper home-slider-container">
<div className="swiper-wrapper">
<div className="swiper-slide">
<div className="home-slider-item">
<a href="#">
<div
className="bg-thumb bg-img"
style={{ backgroundImage: "url(/images/sections/hero-bg.jpg)" }}
/>
<div className="slider-content-area" />
</a>
{slider.items.map((slide) => (
<div key={slide.id} className="swiper-slide">
<div className="home-slider-item">
<SlotLink href={slide.href}>
<div
className="bg-thumb bg-img"
style={{
backgroundImage: `url(${slide.image})`,
...(sliderRatio ? { aspectRatio: sliderRatio } : {}),
}}
/>
<div className="slider-content-area" />
</SlotLink>
</div>
</div>
</div>
))}
</div>
<div className="swiper-btn-wrap">
<div className="swiper-btn-prev">
@@ -68,37 +85,48 @@ export default function HeroSection() {
<section className="features-area py-lg-5 py-2" style={{ zIndex: 1, position: "relative" }}>
<div className="container-xxl">
<div className="row">
{quickLinks.map((link) => (
<div key={link.id} className="col-lg-3 col-md-6 col-12 mb-sm-20 mb-md-4">
<div className="d-flex align-items-start">
<div>
<img alt={link.title} src={link.image} />
</div>
<div className="mr-10">
<a href={link.href} title={link.title}>
{fourIcon.items.map((item, index) => {
const fallback = quickLinks[index];
const title = item.title || fallback?.title || "";
const kicker = item.kicker || fallback?.subtitle || "";
const href = item.href || fallback?.href || "#";
return (
<div key={item.id} className="col-lg-3 col-md-6 col-12 mb-sm-20 mb-md-4">
<div className="d-flex align-items-start">
<div>
<SlotImage
src={item.image}
alt={title}
fallbackSrc={item.fallbackSrc}
/>
</div>
<div className="mr-10">
<SlotLink href={href} className="d-block" title={title}>
<p
className="mb-0"
style={{ letterSpacing: "0px", fontWeight: 600, color: "#16171B" }}
>
{title}
</p>
</SlotLink>
<p
className="mb-0"
style={{ letterSpacing: "0px", fontWeight: 600, color: "#16171B" }}
style={{
letterSpacing: "5px",
fontWeight: 200,
fontSize: "10px",
fontFamily: "montserrat",
color: "#C1C1C1",
}}
>
{link.title}
{kicker}
</p>
</a>
<p
className="mb-0"
style={{
letterSpacing: "5px",
fontWeight: 200,
fontSize: "10px",
fontFamily: "montserrat",
color: "#C1C1C1",
}}
>
{link.subtitle}
</p>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
</div>
</section>
@@ -108,21 +136,22 @@ export default function HeroSection() {
<section className="classical-workout-area">
<div className="container-xxl">
<div className="row align-items-stretch">
<div className="col-lg-6 col-md-6 col-12 mb-3 mb-md-0">
<a href="#online-program" className="d-block h-100 promo-banner">
<img width="100%" alt="" src="/images/sections/online-program.png" />
</a>
</div>
<div className="col-lg-6 col-md-6 col-12">
<a href="#products" className="d-block h-100 promo-banner">
<img width="100%" alt="" src="/images/sections/online-program-mobile.png" />
</a>
</div>
{twoBanner.items.map((item) => (
<div key={item.id} className="col-lg-6 col-md-6 col-12 mb-3 mb-md-0">
<SlotLink href={item.href} className="d-block h-100 promo-banner">
<SlotImage
src={item.image}
alt={item.title || ""}
width="100%"
fallbackSrc={item.fallbackSrc}
/>
</SlotLink>
</div>
))}
</div>
</div>
</section>
</div>
</>
);
}
+39 -17
View File
@@ -1,6 +1,18 @@
import SlotImage from "@/components/SlotImage";
import SlotLink from "@/components/SlotLink";
import { medicinalPlantsText } from "@/data/home";
import type { SlotOverlay } from "@/lib/static-images";
import { parseAspectRatio } from "@/lib/static-images";
type MedicinalPlantsSectionProps = {
banner: SlotOverlay;
twoBanner: SlotOverlay;
};
export default function MedicinalPlantsSection({ banner, twoBanner }: MedicinalPlantsSectionProps) {
const bannerImage = banner.items[0];
const ratio = parseAspectRatio(banner.aspectRatio);
export default function MedicinalPlantsSection() {
return (
<>
<div className="wb-frame" id="medicinal-plants">
@@ -65,12 +77,20 @@ export default function MedicinalPlantsSection() {
</div>
<div className="col-lg-6 order-force-1">
<div className="bg-img text-center">
<img
className="mt-5 mr-5"
style={{ height: "450px" }}
alt="گیاهان دارویی"
src="/images/sections/medicinal-plants.png"
/>
{bannerImage ? (
<SlotImage
className="mt-5 mr-5"
style={{
height: ratio ? undefined : "450px",
width: "auto",
maxWidth: "100%",
...(ratio ? { aspectRatio: ratio, height: "auto", maxHeight: "450px" } : {}),
}}
src={bannerImage.image}
alt={bannerImage.title || "گیاهان دارویی"}
fallbackSrc={bannerImage.fallbackSrc}
/>
) : null}
</div>
</div>
</div>
@@ -82,16 +102,18 @@ export default function MedicinalPlantsSection() {
<section className="classical-workout-area">
<div className="container-xxl">
<div className="row">
<div className="col-lg-6 col-md-6">
<a href="#products" title="لوازم ورزشی">
<img width="100%" alt="لوازم ورزشی" src="/images/sections/medicinal-plants-bg.png" />
</a>
</div>
<div className="col-lg-6 col-md-6">
<a href="#products" title="لوازم ورزشی">
<img width="100%" alt="لوازم ورزشی" src="/images/sections/about.png" />
</a>
</div>
{twoBanner.items.map((item) => (
<div key={item.id} className="col-lg-6 col-md-6">
<SlotLink href={item.href} title={item.title || "لوازم ورزشی"}>
<SlotImage
src={item.image}
alt={item.title || "لوازم ورزشی"}
width="100%"
fallbackSrc={item.fallbackSrc}
/>
</SlotLink>
</div>
))}
</div>
</div>
</section>
+25 -7
View File
@@ -1,6 +1,16 @@
import SlotImage from "@/components/SlotImage";
import { onlineProgramText } from "@/data/home";
import type { SlotOverlay } from "@/lib/static-images";
import { parseAspectRatio } from "@/lib/static-images";
type OnlineProgramSectionProps = {
banner: SlotOverlay;
};
export default function OnlineProgramSection({ banner }: OnlineProgramSectionProps) {
const image = banner.items[0];
const ratio = parseAspectRatio(banner.aspectRatio);
export default function OnlineProgramSection() {
return (
<div className="wb-frame" id="section2">
<section
@@ -59,12 +69,20 @@ export default function OnlineProgramSection() {
</div>
<div className="col-lg-6 order-force-1">
<div className="bg-img text-center">
<img
className="mr-5"
style={{ height: "450px" }}
alt="برنامه آنلاین"
src="/images/sections/rehab-equipment.png"
/>
{image ? (
<SlotImage
className="mr-5"
style={{
height: ratio ? undefined : "450px",
width: "auto",
maxWidth: "100%",
...(ratio ? { aspectRatio: ratio, height: "auto", maxHeight: "450px" } : {}),
}}
src={image.image}
alt={image.title || "برنامه آنلاین"}
fallbackSrc={image.fallbackSrc}
/>
) : null}
</div>
</div>
</div>
+13
View File
@@ -0,0 +1,13 @@
"use client";
import { SessionProvider } from "@/lib/session";
export default function Providers({
businessId,
children,
}: {
businessId: string;
children: React.ReactNode;
}) {
return <SessionProvider businessId={businessId}>{children}</SessionProvider>;
}
+52 -34
View File
@@ -1,6 +1,13 @@
import SlotImage from "@/components/SlotImage";
import SlotLink from "@/components/SlotLink";
import { serviceCards } from "@/data/home";
import type { SlotOverlay } from "@/lib/static-images";
export default function ServicesSection() {
type ServicesSectionProps = {
fourLinkIcon: SlotOverlay;
};
export default function ServicesSection({ fourLinkIcon }: ServicesSectionProps) {
return (
<div className="wb-frame" id="section">
<section className="features-area py-lg-5 py-2">
@@ -13,41 +20,52 @@ export default function ServicesSection() {
style={{ backgroundImage: "url(/images/sections/products-banner.png)" }}
>
<div className="row row-gutter-0">
{serviceCards.map((card) => (
<div key={card.id} className="col-md-6">
<div className="featured-item">
<div className="featured-icon">
<img alt={card.title} src={card.image} />
</div>
<div className="featured-info">
<a href={card.href} title={card.title}>
<h2
style={{
letterSpacing: "0px",
fontWeight: 600,
fontFamily: "iran-sans",
fontSize: "20px",
color: "#000000",
}}
>
{card.title}
</h2>
<p
style={{
letterSpacing: "5px",
fontWeight: 100,
color: "#C1C1C1",
fontFamily: "montserrat",
fontSize: "10px",
}}
>
{card.subtitle}
</p>
</a>
{fourLinkIcon.items.map((item, index) => {
const fallback = serviceCards[index];
const title = item.title || fallback?.title || "";
const kicker = item.kicker || fallback?.subtitle || "";
const href = item.href || fallback?.href || "#";
return (
<div key={item.id} className="col-md-6">
<div className="featured-item">
<div className="featured-icon">
<SlotImage
src={item.image}
alt={title}
fallbackSrc={item.fallbackSrc}
/>
</div>
<div className="featured-info">
<SlotLink href={href} title={title}>
<h2
style={{
letterSpacing: "0px",
fontWeight: 600,
fontFamily: "iran-sans",
fontSize: "20px",
color: "#000000",
}}
>
{title}
</h2>
<p
style={{
letterSpacing: "5px",
fontWeight: 100,
color: "#C1C1C1",
fontFamily: "montserrat",
fontSize: "10px",
}}
>
{kicker}
</p>
</SlotLink>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
</div>
<div className="featured-discount-item">
+36
View File
@@ -0,0 +1,36 @@
import type { CSSProperties } from "react";
type Props = {
src: string;
alt: string;
width?: number | string;
height?: number | string;
className?: string;
style?: CSSProperties;
fallbackSrc?: string;
};
export default function SlotImage({
src,
alt,
width,
height,
className,
style,
fallbackSrc,
}: Props) {
const resolved = src || fallbackSrc || "";
if (!resolved) return null;
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolved}
alt={alt}
width={width}
height={height}
className={className}
style={style}
/>
);
}
+34
View File
@@ -0,0 +1,34 @@
import Link from "next/link";
import type { ReactNode } from "react";
function isExternal(href: string) {
return /^https?:\/\//i.test(href) || href.startsWith("//");
}
export default function SlotLink({
href,
className,
title,
children,
}: {
href?: string;
className?: string;
title?: string;
children: ReactNode;
}) {
if (!href) return <>{children}</>;
if (isExternal(href)) {
return (
<a href={href} className={className} title={title} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
}
return (
<Link href={href} className={className} title={title}>
{children}
</Link>
);
}