/**
* NewsArticleEmbed - Expandable card for news articles shared in forum posts
*
* Features:
* - Collapsible/expandable article view
* - Shows source, title, image, and summary
* - Link to open the original article
* - Visual indication it's a shared news article
*/
'use client';
import { useState } from 'react';
import { ChevronDown, ChevronUp, Newspaper, ExternalLink, Clock } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
export interface NewsArticleData {
id?: string;
title: string;
url: string;
source: string;
sourceName: string;
imageUrl?: string;
summary?: string;
publishedAt?: string;
}
interface NewsArticleEmbedProps {
article: NewsArticleData;
locale?: string;
defaultExpanded?: boolean;
}
// News source logo configuration (matches ActivityCard)
const NEWS_SOURCE_LOGOS: Record<string, { name: string; logo: string; bgColor: string }> = {
cbc: {
name: 'CBC News',
logo: 'https://www.cbc.ca/favicon.ico',
bgColor: 'bg-red-600',
},
globe: {
name: 'Globe & Mail',
logo: 'https://www.theglobeandmail.com/favicon.ico',
bgColor: 'bg-black',
},
national_post: {
name: 'National Post',
logo: 'https://nationalpost.com/favicon.ico',
bgColor: 'bg-red-700',
},
ctv: {
name: 'CTV News',
logo: 'https://www.ctvnews.ca/favicon.ico',
bgColor: 'bg-blue-600',
},
ipolitics: {
name: 'iPolitics',
logo: 'https://ipolitics.ca/favicon.ico',
bgColor: 'bg-slate-800',
},
legisinfo: {
name: 'LEGISinfo',
logo: 'https://www.parl.ca/favicon.ico',
bgColor: 'bg-green-800',
},
};
function NewsSourceLogo({ sourceId, size = 24 }: { sourceId: string; size?: number }) {
const sourceConfig = NEWS_SOURCE_LOGOS[sourceId];
if (!sourceConfig) {
return (
<div
className="flex-shrink-0 rounded-lg flex items-center justify-center bg-emerald-100 dark:bg-emerald-900/30"
style={{ width: size, height: size }}
>
<Newspaper className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
</div>
);
}
return (
<div
className={`flex-shrink-0 rounded-lg flex items-center justify-center overflow-hidden ${sourceConfig.bgColor}`}
style={{ width: size, height: size }}
>
<img
src={sourceConfig.logo}
alt={sourceConfig.name}
className="w-4 h-4 object-contain"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const parent = target.parentElement;
if (parent) {
parent.innerHTML = `<span class="text-white font-bold text-xs">${sourceConfig.name.charAt(0)}</span>`;
}
}}
/>
</div>
);
}
export function NewsArticleEmbed({
article,
locale = 'en',
defaultExpanded = false,
}: NewsArticleEmbedProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const timeAgo = article.publishedAt
? formatDistanceToNow(new Date(article.publishedAt), { addSuffix: true })
: null;
// Truncate text for collapsed view
const truncate = (text: string, maxLength: number) => {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength).trim() + '...';
};
return (
<div className="my-3 bg-gradient-to-br from-emerald-900/20 to-gray-900/50 border border-emerald-700/30 rounded-lg overflow-hidden">
{/* Header - Always visible */}
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setIsExpanded(!isExpanded);
}}
className="w-full flex items-center justify-between p-3 hover:bg-emerald-900/20 transition-colors"
>
<div className="flex items-center gap-2 min-w-0">
<div className="p-1.5 bg-emerald-500/20 rounded-lg flex-shrink-0">
<Newspaper className="w-4 h-4 text-emerald-400" />
</div>
<div className="text-left min-w-0">
<span className="text-sm font-medium text-white">
{locale === 'fr' ? 'Article de presse' : 'News Article'}
</span>
{!isExpanded && (
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">
{truncate(article.title, 60)}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</div>
</button>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-emerald-700/30">
{/* Article Content */}
<div
role="link"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
window.open(article.url, '_blank', 'noopener,noreferrer');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
e.preventDefault();
window.open(article.url, '_blank', 'noopener,noreferrer');
}
}}
className="block hover:bg-emerald-900/10 transition-colors cursor-pointer"
>
<div className="flex gap-4 p-4">
{/* Thumbnail */}
{article.imageUrl && (
<div className="flex-shrink-0 w-24 h-24 sm:w-32 sm:h-24 rounded-lg overflow-hidden bg-gray-800">
<img
src={article.imageUrl}
alt=""
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).parentElement!.style.display = 'none';
}}
/>
</div>
)}
{/* Content */}
<div className="flex-1 min-w-0">
{/* Source and time */}
<div className="flex items-center gap-2 mb-2 flex-wrap">
<NewsSourceLogo sourceId={article.source} size={20} />
<span className="text-xs font-medium text-gray-400">
{article.sourceName}
</span>
{timeAgo && (
<>
<span className="text-gray-600">•</span>
<span className="text-xs text-gray-500 flex items-center gap-1">
<Clock size={10} />
{timeAgo}
</span>
</>
)}
</div>
{/* Title */}
<h4 className="text-sm font-semibold text-white mb-2 line-clamp-2 group-hover:text-emerald-400 transition-colors">
{article.title}
</h4>
{/* Summary */}
{article.summary && (
<p className="text-xs text-gray-400 line-clamp-3">
{article.summary}
</p>
)}
</div>
</div>
</div>
{/* Footer */}
<div className="px-4 py-3 bg-gray-900/50 border-t border-emerald-700/30 flex items-center justify-between">
<p className="text-xs text-gray-500 italic">
{locale === 'fr'
? 'Article partagé depuis CanadaGPT'
: 'Article shared via CanadaGPT'}
</p>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
window.open(article.url, '_blank', 'noopener,noreferrer');
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-400 rounded-lg text-xs font-medium transition-colors"
>
<ExternalLink className="w-3.5 h-3.5" />
<span>{locale === 'fr' ? 'Lire l\'article' : 'Read Article'}</span>
</button>
</div>
</div>
)}
</div>
);
}