/**
* ActiveDiscussionsCarousel Component
*
* Auto-scrolling carousel showing active discussions
* Displays 3 tiles at a time, auto-advances every 3-4 seconds
* Supports swipe gestures on mobile
*/
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import Link from 'next/link';
import { getPosts } from '@/actions/forum';
import type { ForumPost } from '@/types/forum';
import { MessageSquare, ArrowUp, ChevronLeft, ChevronRight, Flame } from 'lucide-react';
import { cn } from '@canadagpt/design-system';
import { formatDistanceToNow } from 'date-fns';
interface ActiveDiscussionsCarouselProps {
locale?: string;
limit?: number;
autoScrollInterval?: number; // milliseconds
}
/**
* Loading skeleton for discussion tiles
*/
function DiscussionSkeleton() {
return (
<div className="flex-shrink-0 w-[280px] sm:w-[320px] p-4 rounded-lg bg-bg-elevated border border-border-subtle animate-pulse">
<div className="h-4 w-3/4 bg-bg-overlay rounded mb-3" />
<div className="h-3 w-full bg-bg-overlay rounded mb-2" />
<div className="h-3 w-2/3 bg-bg-overlay rounded mb-3" />
<div className="flex gap-3">
<div className="h-3 w-12 bg-bg-overlay rounded" />
<div className="h-3 w-12 bg-bg-overlay rounded" />
</div>
</div>
);
}
/**
* Individual discussion tile
*/
function DiscussionTile({ post, locale }: { post: ForumPost; locale: string }) {
const timeAgo = formatDistanceToNow(new Date(post.last_reply_at || post.created_at), {
addSuffix: true,
});
const href =
post.post_type === 'bill_comment'
? `/${locale}/bills/${post.bill_session}/${post.bill_number}#post-${post.id}`
: `/${locale}/forum/posts/${post.id}`;
return (
<Link
href={href}
className="flex-shrink-0 w-[280px] sm:w-[320px] p-4 rounded-lg bg-bg-elevated border border-border-subtle hover:border-accent-red hover:shadow-md transition-all group"
>
{/* Category/Bill badge */}
<div className="flex items-center gap-2 mb-2">
{post.post_type === 'bill_comment' && post.bill_number ? (
<span className="text-xs font-mono px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
{post.bill_number.toUpperCase()}
</span>
) : post.category?.color ? (
<span
className="text-xs px-2 py-0.5 rounded font-medium"
style={{
backgroundColor: post.category.color + '20',
color: post.category.color,
}}
>
{post.category.name}
</span>
) : null}
<span className="text-xs text-text-tertiary ml-auto">{timeAgo}</span>
</div>
{/* Title */}
<h4 className="font-medium text-text-primary group-hover:text-accent-red transition-colors line-clamp-2 mb-2 text-sm">
{post.title || post.content.substring(0, 80)}
</h4>
{/* Author */}
<p className="text-xs text-text-secondary mb-3 truncate">
{locale === 'fr' ? 'par' : 'by'} {post.author_name}
</p>
{/* Stats */}
<div className="flex items-center gap-4 text-xs text-text-tertiary">
<span className="flex items-center gap-1">
<ArrowUp size={12} className="text-green-500" />
{post.upvotes_count}
</span>
<span className="flex items-center gap-1">
<MessageSquare size={12} />
{post.reply_count}
</span>
{post.reply_count > 5 && (
<span className="flex items-center gap-1 text-amber-500">
<Flame size={12} />
{locale === 'fr' ? 'Actif' : 'Active'}
</span>
)}
</div>
</Link>
);
}
export function ActiveDiscussionsCarousel({
locale = 'en',
limit = 9,
autoScrollInterval = 4000, // 4 seconds default
}: ActiveDiscussionsCarouselProps) {
const [posts, setPosts] = useState<ForumPost[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [currentIndex, setCurrentIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const touchStartX = useRef<number>(0);
// Fetch hot discussions
useEffect(() => {
const loadDiscussions = async () => {
const result = await getPosts({
sort: 'hot',
limit,
offset: 0,
});
if (result.success && result.data) {
setPosts(result.data.data);
}
setIsLoading(false);
};
loadDiscussions();
}, [limit]);
// Calculate how many "pages" of 3 tiles we have
const tilesPerPage = 3;
const totalPages = Math.ceil(posts.length / tilesPerPage);
// Auto-scroll
useEffect(() => {
if (isPaused || totalPages <= 1) return;
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % totalPages);
}, autoScrollInterval);
return () => clearInterval(interval);
}, [isPaused, totalPages, autoScrollInterval]);
// Scroll to current page
useEffect(() => {
if (containerRef.current && posts.length > 0) {
const tileWidth = 320 + 12; // tile width + gap
containerRef.current.scrollTo({
left: currentIndex * tilesPerPage * tileWidth,
behavior: 'smooth',
});
}
}, [currentIndex, posts.length]);
// Navigation handlers
const goToPage = useCallback(
(page: number) => {
setCurrentIndex(Math.max(0, Math.min(page, totalPages - 1)));
},
[totalPages]
);
const goNext = useCallback(() => {
setCurrentIndex((prev) => (prev + 1) % totalPages);
}, [totalPages]);
const goPrev = useCallback(() => {
setCurrentIndex((prev) => (prev - 1 + totalPages) % totalPages);
}, [totalPages]);
// Touch handlers for swipe
const handleTouchStart = (e: React.TouchEvent) => {
touchStartX.current = e.touches[0].clientX;
setIsPaused(true);
};
const handleTouchEnd = (e: React.TouchEvent) => {
const touchEndX = e.changedTouches[0].clientX;
const diff = touchStartX.current - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
goNext();
} else {
goPrev();
}
}
// Resume auto-scroll after a delay
setTimeout(() => setIsPaused(false), 5000);
};
if (isLoading) {
return (
<div className="px-4 py-4 bg-bg-secondary/50">
<div className="max-w-3xl mx-auto">
<div className="flex items-center gap-2 mb-3">
<Flame className="text-accent-red" size={18} />
<h3 className="text-sm font-semibold text-text-primary">
{locale === 'fr' ? 'Discussions actives' : 'Active Discussions'}
</h3>
</div>
<div className="flex gap-3 overflow-hidden">
<DiscussionSkeleton />
<DiscussionSkeleton />
<DiscussionSkeleton />
</div>
</div>
</div>
);
}
if (posts.length === 0) {
return null;
}
return (
<div
className="px-4 py-4 bg-bg-secondary/50 border-b border-border-subtle"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<div className="max-w-3xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Flame className="text-accent-red" size={18} />
<h3 className="text-sm font-semibold text-text-primary">
{locale === 'fr' ? 'Discussions actives' : 'Active Discussions'}
</h3>
</div>
{/* Navigation controls */}
{totalPages > 1 && (
<div className="flex items-center gap-2">
<button
onClick={goPrev}
className="p-1 rounded hover:bg-bg-elevated text-text-tertiary hover:text-text-primary transition-colors"
aria-label="Previous"
>
<ChevronLeft size={18} />
</button>
{/* Page indicators */}
<div className="flex gap-1">
{Array.from({ length: totalPages }).map((_, i) => (
<button
key={i}
onClick={() => goToPage(i)}
className={cn(
'w-2 h-2 rounded-full transition-all',
i === currentIndex
? 'bg-accent-red w-4'
: 'bg-bg-overlay hover:bg-text-tertiary'
)}
aria-label={`Page ${i + 1}`}
/>
))}
</div>
<button
onClick={goNext}
className="p-1 rounded hover:bg-bg-elevated text-text-tertiary hover:text-text-primary transition-colors"
aria-label="Next"
>
<ChevronRight size={18} />
</button>
</div>
)}
</div>
{/* Carousel */}
<div
ref={containerRef}
className="flex gap-3 overflow-x-auto scrollbar-hide scroll-smooth"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{posts.map((post) => (
<DiscussionTile key={post.id} post={post} locale={locale} />
))}
</div>
{/* View all link */}
<div className="mt-3 text-center">
<Link
href={`/${locale}/forum/discussions?sort=hot`}
className="text-xs text-accent-red hover:underline"
>
{locale === 'fr' ? 'Voir toutes les discussions →' : 'View all discussions →'}
</Link>
</div>
</div>
</div>
);
}
export default ActiveDiscussionsCarousel;