Files
OllaHub/components/chat/sources-carousel.tsx

155 lines
5.8 KiB
TypeScript

'use client';
import { Card } from '@/components/ui/card';
import { ExternalLink } from 'lucide-react';
import { ScrapedContent } from '@/services/webSearch';
import { useRef, useState } from 'react';
import { cn } from '@/lib/utils';
interface SourcesCarouselProps {
sources: ScrapedContent[];
}
export function SourcesCarousel({ sources }: SourcesCarouselProps) {
if (!sources || sources.length === 0) return null;
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [hasMoved, setHasMoved] = useState(false);
// Handlers para mouse drag
const handleMouseDown = (e: React.MouseEvent) => {
if (!scrollContainerRef.current) return;
setIsDragging(true);
setStartX(e.pageX - scrollContainerRef.current.offsetLeft);
setScrollLeft(scrollContainerRef.current.scrollLeft);
setHasMoved(false);
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging || !scrollContainerRef.current) return;
e.preventDefault();
const x = e.pageX - scrollContainerRef.current.offsetLeft;
const walk = (x - startX) * 1.5; // Velocidade do scroll
scrollContainerRef.current.scrollLeft = scrollLeft - walk;
// Se moveu mais de 3px, marca como movimento (não clique)
if (Math.abs(walk) > 3) {
setHasMoved(true);
}
};
const handleMouseUp = () => {
setIsDragging(false);
// Resetar após um pequeno delay
setTimeout(() => setHasMoved(false), 150);
};
const handleMouseLeave = () => {
setIsDragging(false);
setTimeout(() => setHasMoved(false), 150);
};
// Handlers para touch drag (mobile)
const handleTouchStart = (e: React.TouchEvent) => {
if (!scrollContainerRef.current) return;
setIsDragging(true);
setStartX(e.touches[0].pageX - scrollContainerRef.current.offsetLeft);
setScrollLeft(scrollContainerRef.current.scrollLeft);
setHasMoved(false);
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!isDragging || !scrollContainerRef.current) return;
const x = e.touches[0].pageX - scrollContainerRef.current.offsetLeft;
const walk = (x - startX) * 1.5;
scrollContainerRef.current.scrollLeft = scrollLeft - walk;
if (Math.abs(walk) > 3) {
setHasMoved(true);
}
};
const handleTouchEnd = () => {
setIsDragging(false);
setTimeout(() => setHasMoved(false), 150);
};
// Prevenir navegação do link durante drag
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (hasMoved || isDragging) {
e.preventDefault();
e.stopPropagation();
}
};
return (
<div className="w-full max-w-3xl mx-auto mb-4 mt-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 px-1">
Fontes Analisadas
</h3>
<div
ref={scrollContainerRef}
className={cn(
"flex gap-3 overflow-x-auto pb-4 scrollbar-hide -mx-4 px-4 sm:mx-0 sm:px-0 mask-fade-sides",
"cursor-grab active:cursor-grabbing select-none touch-pan-x",
isDragging && "cursor-grabbing"
)}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
scrollBehavior: isDragging ? 'auto' : 'smooth',
WebkitOverflowScrolling: 'touch' // Smooth scrolling no iOS
}}
>
{sources.map((source, idx) => {
const hostname = new URL(source.url).hostname.replace('www.', '');
const faviconUrl = `https://www.google.com/s2/favicons?domain=${hostname}&sz=32`;
return (
<a
key={idx}
href={source.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleLinkClick}
className="flex-none w-40 group transition-all hover:-translate-y-1 block"
style={{ pointerEvents: (isDragging || hasMoved) ? 'none' : 'auto' }}
>
<Card className="h-24 p-3 flex flex-col justify-between hover:shadow-md hover:border-primary/20 transition-colors bg-card/50 backdrop-blur-sm cursor-pointer overflow-hidden">
<div className="flex items-start justify-between min-h-0 flex-shrink-0">
<img
src={faviconUrl}
alt=""
className="w-4 h-4 rounded-sm opacity-70 group-hover:opacity-100 transition-opacity flex-shrink-0"
onError={(e) => (e.target as HTMLImageElement).style.display = 'none'}
/>
<ExternalLink className="w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 ml-1" />
</div>
<div className="space-y-1 min-w-0 flex-1 overflow-hidden flex flex-col justify-end">
<div className="text-xs font-medium leading-tight line-clamp-2 text-card-foreground/90 group-hover:text-primary transition-colors break-words overflow-hidden text-ellipsis">
{source.title}
</div>
<div className="text-[10px] text-muted-foreground truncate overflow-hidden text-ellipsis">
{hostname}
</div>
</div>
</Card>
</a>
);
})}
</div>
</div>
);
}