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:
co-authored by
Cursor
parent
424e339986
commit
7e8a1ef7dc
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user