@@ -45,72 +35,7 @@ export default function FeaturedEvents({ content, events, isArchive }: FeaturedE
{events.length ? (
-
- {events.map((event) => {
- const candidateImage = getEventCardImageUrl(event);
- const hasImage = candidateImage !== "/placeholder.svg";
- const image = hasImage ? toAbsoluteUrl(candidateImage, apiBaseUrl) : undefined;
-
- return (
-
-
- {image ? (
-
- ) : (
-
-
-
- )}
- {isArchive ? (
-
- از آرشیو
-
- ) : null}
-
-
-
-
- {event.title}
-
-
-
-
-
- {event.description}
-
-
-
-
-
-
- {(event.location || event.address || event.event_type_label) ? (
-
-
- {event.location || event.address || event.event_type_label}
-
- ) : null}
-
-
-
-
- {content.featured_event_action_label}
-
-
-
-
- );
- })}
-
+
) : (
در حال حاضر رویدادی برای نمایش نیست.
diff --git a/src/components/home/FeaturedEventsCarousel.tsx b/src/components/home/FeaturedEventsCarousel.tsx
new file mode 100644
index 0000000..519faa6
--- /dev/null
+++ b/src/components/home/FeaturedEventsCarousel.tsx
@@ -0,0 +1,170 @@
+"use client";
+
+import { useCallback, useEffect, useRef } from "react";
+import type { MouseEvent as ReactMouseEvent } from "react";
+import { EventCard } from "@/components/events/EventCard";
+import type { EventListItemSchema } from "@/lib/types";
+
+type FeaturedEventsCarouselProps = {
+ events: EventListItemSchema[];
+ now: number;
+};
+
+export function FeaturedEventsCarousel({ events, now }: FeaturedEventsCarouselProps) {
+ const trackRef = useRef(null);
+ const cardRefs = useRef>([]);
+ const progressTrackRefs = useRef>([]);
+ const progressThumbRefs = useRef>([]);
+ const dragRef = useRef<{ startX: number; startY: number; startScrollLeft: number; moved: boolean; cancelled: boolean } | null>(null);
+ const suppressClickRef = useRef(false);
+ const hasMobileSlides = events.length > 1;
+ const hasTabletSlides = events.length > 2;
+
+ const updateScrollProgress = useCallback(() => {
+ const track = trackRef.current;
+ if (!track) return;
+
+ const maxScroll = Math.max(0, track.scrollWidth - track.clientWidth);
+ const firstCard = cardRefs.current[0];
+ const trackRight = track.getBoundingClientRect().right;
+ const offset = firstCard ? Math.max(0, firstCard.getBoundingClientRect().right - trackRight) : 0;
+ const progress = maxScroll ? Math.min(1, offset / maxScroll) : 0;
+ const thumbWidth = 100 / events.length;
+ const thumbRight = progress * (100 - thumbWidth);
+
+ progressThumbRefs.current.forEach((thumb) => {
+ if (!thumb) return;
+ thumb.style.width = `${thumbWidth}%`;
+ thumb.style.right = `${thumbRight}%`;
+ });
+ progressTrackRefs.current.forEach((progressTrack) => {
+ progressTrack?.setAttribute("aria-valuenow", String(Math.round(progress * 100)));
+ });
+ }, [events.length]);
+
+ useEffect(() => {
+ updateScrollProgress();
+ window.addEventListener("resize", updateScrollProgress);
+ return () => window.removeEventListener("resize", updateScrollProgress);
+ }, [updateScrollProgress]);
+
+ useEffect(() => {
+ const handleMouseMove = (event: MouseEvent) => {
+ const drag = dragRef.current;
+ const track = trackRef.current;
+ if (!drag || !track || drag.cancelled) return;
+
+ const deltaX = event.clientX - drag.startX;
+ const deltaY = event.clientY - drag.startY;
+ if (!drag.moved && Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 6) return;
+ if (!drag.moved && Math.abs(deltaY) > Math.abs(deltaX)) {
+ drag.cancelled = true;
+ track.removeAttribute("data-dragging");
+ return;
+ }
+
+ drag.moved = true;
+ event.preventDefault();
+ track.scrollLeft = drag.startScrollLeft - deltaX;
+ };
+
+ const handleMouseUp = () => {
+ const drag = dragRef.current;
+ if (!drag) return;
+
+ dragRef.current = null;
+ trackRef.current?.removeAttribute("data-dragging");
+ if (drag.moved || drag.cancelled) {
+ suppressClickRef.current = true;
+ window.setTimeout(() => {
+ suppressClickRef.current = false;
+ }, 0);
+ }
+ };
+
+ window.addEventListener("mousemove", handleMouseMove);
+ window.addEventListener("mouseup", handleMouseUp);
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove);
+ window.removeEventListener("mouseup", handleMouseUp);
+ };
+ }, []);
+
+ const startDragging = (event: ReactMouseEvent) => {
+ const isMobileSlider = hasMobileSlides && window.matchMedia("(max-width: 639px)").matches;
+ const isTabletSlider = hasTabletSlides && window.matchMedia("(min-width: 640px) and (max-width: 1279px)").matches;
+ if ((!isMobileSlider && !isTabletSlider) || event.button !== 0) return;
+
+ dragRef.current = {
+ startX: event.clientX,
+ startY: event.clientY,
+ startScrollLeft: event.currentTarget.scrollLeft,
+ moved: false,
+ cancelled: false,
+ };
+ event.currentTarget.dataset.dragging = "true";
+ };
+
+ const handleClickCapture = (event: ReactMouseEvent) => {
+ if (!suppressClickRef.current) return;
+ event.preventDefault();
+ event.stopPropagation();
+ suppressClickRef.current = false;
+ };
+
+ const renderProgress = (index: number, className: string) => (
+ { progressTrackRefs.current[index] = element; }}
+ className={`relative h-1.5 w-full overflow-hidden rounded-full bg-primary/10 ${className}`}
+ role="progressbar"
+ aria-label="موقعیت در رویدادهای ویژه"
+ aria-valuemin={0}
+ aria-valuemax={100}
+ aria-valuenow={0}
+ >
+ { progressThumbRefs.current[index] = element; }}
+ className="absolute inset-y-0 right-0 rounded-full bg-primary"
+ style={{ width: `${100 / events.length}%` }}
+ />
+
+ );
+
+ const trackClassName = `grid gap-3 sm:gap-4 xl:grid-cols-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
+ hasMobileSlides
+ ? "max-sm:flex max-sm:overflow-x-auto max-sm:touch-pan-x"
+ : "grid-cols-1"
+ } ${
+ hasTabletSlides
+ ? "sm:max-xl:flex sm:max-xl:overflow-x-auto sm:max-xl:touch-pan-x"
+ : "sm:grid-cols-2"
+ }`;
+
+ return (
+ <>
+ event.preventDefault()}
+ className={trackClassName}
+ >
+ {events.map((event, index) => (
+
{ cardRefs.current[index] = element; }}
+ className={`min-w-0 ${hasMobileSlides ? "max-sm:basis-[85%] max-sm:shrink-0" : ""} ${hasTabletSlides ? "sm:max-xl:basis-[calc(50%_-_0.5rem)] sm:max-xl:shrink-0" : ""}`}
+ >
+
+
+ ))}
+
+ {hasMobileSlides ? renderProgress(0, "mt-5 sm:hidden") : null}
+ {hasTabletSlides ? renderProgress(1, "mt-5 hidden sm:block xl:hidden") : null}
+ >
+ );
+}
diff --git a/src/views/Home.tsx b/src/views/Home.tsx
index be33e00..2014cea 100644
--- a/src/views/Home.tsx
+++ b/src/views/Home.tsx
@@ -12,10 +12,9 @@ type HomeProps = {
siteContent: SiteContent;
events: EventListItemSchema[];
posts: PostListSchema[];
- isEventArchive: boolean;
};
-export default function Home({ content, siteContent, events, posts, isEventArchive }: HomeProps) {
+export default function Home({ content, siteContent, events, posts }: HomeProps) {
const heroImage = content.hero_image || siteContent.logo || {
url: "/favicon.ico",
alt: siteContent.short_name || "نشان انجمن",
@@ -76,7 +75,7 @@ export default function Home({ content, siteContent, events, posts, isEventArchi
-