Connect Meshkee-managed imagery and publish sitemap metadata
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
e664acb854
commit
00126e5777
+180
-22
@@ -27,10 +27,24 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { FormEvent } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
cssAspectRatio,
|
||||
fetchHomeStaticImageSlots,
|
||||
type StaticImageSlot,
|
||||
} from "./lib/meshkee";
|
||||
|
||||
const assetBase = "/assets";
|
||||
|
||||
const heroSlides = [
|
||||
type HeroSlide = {
|
||||
image: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
linkUrl?: string;
|
||||
aspectRatio?: string;
|
||||
};
|
||||
|
||||
const heroSlides: HeroSlide[] = [
|
||||
{
|
||||
image: `${assetBase}/i8SsUDDMLv.jpg`,
|
||||
eyebrow: "WIRE & CABLE MACHINERY",
|
||||
@@ -164,15 +178,83 @@ export default function Home() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const [query, setQuery] = useState("");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [staticImageSlots, setStaticImageSlots] = useState<StaticImageSlot[]>([]);
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const slotsByKey = useMemo(
|
||||
() => new Map(staticImageSlots.map((slot) => [slot.key, slot])),
|
||||
[staticImageSlots],
|
||||
);
|
||||
|
||||
const activeHeroSlides = useMemo<HeroSlide[]>(() => {
|
||||
const slot = slotsByKey.get("slider");
|
||||
if (!slot?.images.length) return heroSlides;
|
||||
|
||||
return slot.images.map((image, index) => {
|
||||
const fallback = heroSlides[index % heroSlides.length];
|
||||
return {
|
||||
image: image.url || fallback.image,
|
||||
eyebrow: image.titleEn || fallback.eyebrow,
|
||||
title: image.titleFa || fallback.title,
|
||||
text: image.subtext || fallback.text,
|
||||
linkUrl: image.linkUrl || undefined,
|
||||
aspectRatio: cssAspectRatio(slot.aspectRatio),
|
||||
};
|
||||
});
|
||||
}, [slotsByKey]);
|
||||
|
||||
const activeCategories = useMemo(() => {
|
||||
const slot = slotsByKey.get("three-categories");
|
||||
return categories.map((fallback, index) => {
|
||||
const image = slot?.images[index];
|
||||
if (!image) {
|
||||
return {
|
||||
...fallback,
|
||||
linkUrl: undefined,
|
||||
aspectRatio: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...fallback,
|
||||
image: image.url || fallback.image,
|
||||
title: image.titleFa || fallback.title,
|
||||
en: image.titleEn || fallback.en,
|
||||
count: image.subtext || fallback.count,
|
||||
linkUrl: image.linkUrl || undefined,
|
||||
aspectRatio: cssAspectRatio(slot?.aspectRatio),
|
||||
};
|
||||
});
|
||||
}, [slotsByKey]);
|
||||
|
||||
const aboutImages = slotsByKey.get("about-us-images");
|
||||
const aboutMainImage = aboutImages?.images[0];
|
||||
const aboutSecondaryImage = aboutImages?.images[1];
|
||||
const activeHeroIndex = activeSlide % activeHeroSlides.length;
|
||||
const activeHero = activeHeroSlides[activeHeroIndex];
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetchHomeStaticImageSlots(controller.signal)
|
||||
.then(setStaticImageSlots)
|
||||
.catch(() => {
|
||||
// Keep the local image fallbacks when the API is unavailable.
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeHeroSlides.length <= 1) return;
|
||||
|
||||
const timer = window.setInterval(
|
||||
() => setActiveSlide((current) => (current + 1) % heroSlides.length),
|
||||
() =>
|
||||
setActiveSlide((current) => (current + 1) % activeHeroSlides.length),
|
||||
6500,
|
||||
);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
}, [activeHeroSlides.length]);
|
||||
|
||||
const results = useMemo(() => {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
@@ -281,20 +363,32 @@ export default function Home() {
|
||||
</header>
|
||||
|
||||
<section className="hero">
|
||||
{heroSlides.map((slide, index) => (
|
||||
{activeHeroSlides.map((slide, index) => (
|
||||
<div
|
||||
className={`hero__slide ${index === activeSlide ? "is-active" : ""}`}
|
||||
key={slide.image}
|
||||
style={{ backgroundImage: `url("${slide.image}")` }}
|
||||
/>
|
||||
className={`hero__slide ${index === activeHeroIndex ? "is-active" : ""}`}
|
||||
key={`${slide.image}-${index}`}
|
||||
style={{ aspectRatio: slide.aspectRatio }}
|
||||
>
|
||||
{slide.linkUrl ? (
|
||||
<a
|
||||
className="hero__slide-link"
|
||||
href={slide.linkUrl}
|
||||
aria-label={slide.title}
|
||||
>
|
||||
<img src={slide.image} alt={slide.title} />
|
||||
</a>
|
||||
) : (
|
||||
<img src={slide.image} alt={slide.title} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="hero__overlay" />
|
||||
<div className="hero__pattern" />
|
||||
<div className="container hero__content">
|
||||
<div className="hero__copy">
|
||||
<p className="eyebrow">{heroSlides[activeSlide].eyebrow}</p>
|
||||
<h1>{heroSlides[activeSlide].title}</h1>
|
||||
<p className="hero__description">{heroSlides[activeSlide].text}</p>
|
||||
<p className="eyebrow">{activeHero.eyebrow}</p>
|
||||
<h1>{activeHero.title}</h1>
|
||||
<p className="hero__description">{activeHero.text}</p>
|
||||
<div className="hero__buttons">
|
||||
<a className="button button--red" href="#products">
|
||||
مشاهده محصولات
|
||||
@@ -321,7 +415,8 @@ export default function Home() {
|
||||
<button
|
||||
onClick={() =>
|
||||
setActiveSlide(
|
||||
(activeSlide - 1 + heroSlides.length) % heroSlides.length,
|
||||
(activeHeroIndex - 1 + activeHeroSlides.length) %
|
||||
activeHeroSlides.length,
|
||||
)
|
||||
}
|
||||
aria-label="اسلاید قبلی"
|
||||
@@ -329,17 +424,19 @@ export default function Home() {
|
||||
<ChevronRight />
|
||||
</button>
|
||||
<div>
|
||||
{heroSlides.map((slide, index) => (
|
||||
{activeHeroSlides.map((slide, index) => (
|
||||
<button
|
||||
className={index === activeSlide ? "active" : ""}
|
||||
key={slide.image}
|
||||
className={index === activeHeroIndex ? "active" : ""}
|
||||
key={`${slide.image}-${index}`}
|
||||
onClick={() => setActiveSlide(index)}
|
||||
aria-label={`اسلاید ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActiveSlide((activeSlide + 1) % heroSlides.length)}
|
||||
onClick={() =>
|
||||
setActiveSlide((activeHeroIndex + 1) % activeHeroSlides.length)
|
||||
}
|
||||
aria-label="اسلاید بعدی"
|
||||
>
|
||||
<ChevronLeft />
|
||||
@@ -401,7 +498,7 @@ export default function Home() {
|
||||
{results.length ? (
|
||||
results.map((product) => (
|
||||
<a href="#products" key={product.id}>
|
||||
<img src={product.image} alt="" />
|
||||
<img src={product.image} alt={product.title} />
|
||||
<div>
|
||||
<strong>{product.title}</strong>
|
||||
<small dir="ltr">{product.en}</small>
|
||||
@@ -428,8 +525,17 @@ export default function Home() {
|
||||
title="سه مسیر، یک راهکار کامل صنعتی"
|
||||
/>
|
||||
<div className="category-grid">
|
||||
{categories.map((category, index) => (
|
||||
<a className="category-card" href="#" key={category.title}>
|
||||
{activeCategories.map((category, index) => (
|
||||
<a
|
||||
className="category-card"
|
||||
href={category.linkUrl || "#"}
|
||||
key={`${category.title}-${index}`}
|
||||
style={
|
||||
category.aspectRatio
|
||||
? { aspectRatio: category.aspectRatio, height: "auto" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<img src={category.image} alt={category.title} />
|
||||
<div className="category-card__shade" />
|
||||
<span className="category-card__number">0{index + 1}</span>
|
||||
@@ -487,9 +593,61 @@ export default function Home() {
|
||||
<section className="about" id="about">
|
||||
<div className="container about__grid">
|
||||
<div className="about__visual">
|
||||
<img src={`${assetBase}/trEfzlNgmz.jpg`} alt="تجهیزات صنعتی سامان اندیش" />
|
||||
<div className="about__small-image">
|
||||
<img src={`${assetBase}/rhipZeVqzD.jpg`} alt="ماشینآلات صنعت کابل" />
|
||||
{aboutMainImage?.linkUrl ? (
|
||||
<a
|
||||
className="about__main-image"
|
||||
href={aboutMainImage.linkUrl}
|
||||
style={{
|
||||
aspectRatio: cssAspectRatio(aboutImages?.aspectRatio),
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={aboutMainImage.url}
|
||||
alt={aboutMainImage.titleFa || "تجهیزات صنعتی سامان اندیش"}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<img
|
||||
src={aboutMainImage?.url || `${assetBase}/trEfzlNgmz.jpg`}
|
||||
alt={aboutMainImage?.titleFa || "تجهیزات صنعتی سامان اندیش"}
|
||||
style={
|
||||
aboutMainImage
|
||||
? {
|
||||
aspectRatio: cssAspectRatio(aboutImages?.aspectRatio),
|
||||
height: "auto",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="about__small-image"
|
||||
style={
|
||||
aboutSecondaryImage
|
||||
? {
|
||||
aspectRatio: cssAspectRatio(aboutImages?.aspectRatio),
|
||||
height: "auto",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{aboutSecondaryImage?.linkUrl ? (
|
||||
<a href={aboutSecondaryImage.linkUrl}>
|
||||
<img
|
||||
src={aboutSecondaryImage.url}
|
||||
alt={aboutSecondaryImage.titleFa || "ماشینآلات صنعت کابل"}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<img
|
||||
src={
|
||||
aboutSecondaryImage?.url || `${assetBase}/rhipZeVqzD.jpg`
|
||||
}
|
||||
alt={
|
||||
aboutSecondaryImage?.titleFa || "ماشینآلات صنعت کابل"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="experience-badge">
|
||||
<strong>۲۵</strong>
|
||||
|
||||
Reference in New Issue
Block a user