Spaces:
Running
Running
| import { useState } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { ChevronDown, ExternalLink, Hash, FileText } from 'lucide-react'; | |
| import { cn } from '../lib/utils'; | |
| export default function ClusterCard({ cluster }) { | |
| const [isExpanded, setIsExpanded] = useState(false); | |
| return ( | |
| <motion.div | |
| layout | |
| className="bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden hover:border-slate-700 transition-colors" | |
| > | |
| <div | |
| onClick={() => setIsExpanded(!isExpanded)} | |
| className="p-5 cursor-pointer flex items-start justify-between gap-4" | |
| > | |
| <div className="flex-1"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <span className="bg-blue-500/10 text-blue-400 text-xs px-2 py-0.5 rounded-full border border-blue-500/20 font-medium flex items-center gap-1"> | |
| <Hash className="w-3 h-3" /> | |
| Cluster {cluster.cluster_id} | |
| </span> | |
| <span className="bg-slate-800 text-slate-400 text-xs px-2 py-0.5 rounded-full font-medium flex items-center gap-1"> | |
| <FileText className="w-3 h-3" /> | |
| {cluster.size} articles | |
| </span> | |
| </div> | |
| <h3 className="text-lg font-semibold text-slate-200 leading-tight group-hover:text-blue-400 transition-colors"> | |
| {cluster.title} | |
| </h3> | |
| </div> | |
| <motion.div | |
| animate={{ rotate: isExpanded ? 180 : 0 }} | |
| transition={{ duration: 0.2 }} | |
| className="bg-slate-800 p-1.5 rounded-lg text-slate-400" | |
| > | |
| <ChevronDown className="w-5 h-5" /> | |
| </motion.div> | |
| </div> | |
| <AnimatePresence> | |
| {isExpanded && ( | |
| <motion.div | |
| initial={{ height: 0, opacity: 0 }} | |
| animate={{ height: "auto", opacity: 1 }} | |
| exit={{ height: 0, opacity: 0 }} | |
| className="border-t border-slate-800 bg-slate-900/30" | |
| > | |
| <div className="p-4 space-y-3"> | |
| {cluster.articles.map((article, idx) => ( | |
| <a | |
| key={idx} | |
| href={article.url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="block p-3 rounded-lg bg-slate-800/40 hover:bg-slate-800 transition-colors group" | |
| > | |
| <div className="flex justify-between items-start gap-3"> | |
| <div className="flex-1"> | |
| <h4 className="text-sm text-slate-300 font-medium line-clamp-2 group-hover:text-blue-300 mb-1"> | |
| {article.title} | |
| </h4> | |
| <div className="flex flex-wrap gap-2"> | |
| {article.is_clickbait && ( | |
| <span className="text-[10px] uppercase font-bold text-red-400 bg-red-900/30 border border-red-900/50 px-1.5 py-0.5 rounded"> | |
| Clickbait | |
| </span> | |
| )} | |
| {article.is_sensationalist && ( | |
| <span className="text-[10px] uppercase font-bold text-amber-400 bg-amber-900/30 border border-amber-900/50 px-1.5 py-0.5 rounded"> | |
| Sensationalist | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <ExternalLink className="w-4 h-4 text-slate-500 shrink-0 group-hover:text-blue-400 mt-1" /> | |
| </div> | |
| <div className="mt-2 flex items-center gap-2 text-xs text-slate-500"> | |
| <span className="font-semibold text-slate-400">{article.newspaper}</span> | |
| <span>•</span> | |
| <span className="truncate max-w-[200px]">{article.newspaper_url}</span> | |
| </div> | |
| </a> | |
| ))} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </motion.div> | |
| ); | |
| } | |