120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { ArrowRight, Info } from 'lucide-react';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
import type { SearchResult } from '@/app/api/search/route';
|
|
|
|
interface RelatedArticlesProps {
|
|
title: string;
|
|
currentSlug: string;
|
|
}
|
|
|
|
export function RelatedArticles({ title, currentSlug }: RelatedArticlesProps) {
|
|
const [articles, setArticles] = useState<SearchResult[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchRelatedArticles = async () => {
|
|
try {
|
|
const response = await fetch(`/api/search?query=${encodeURIComponent(title)}`);
|
|
if (!response.ok) throw new Error('Failed to fetch related articles');
|
|
|
|
const data = await response.json();
|
|
const results = data.results || [];
|
|
|
|
// Filter out the current article and take top 5
|
|
const filtered = results
|
|
.filter((article: SearchResult) =>
|
|
article.title.replace(/ /g, '_').toLowerCase() !==
|
|
currentSlug.toLowerCase()
|
|
)
|
|
.slice(0, 5);
|
|
|
|
setArticles(filtered);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRelatedArticles();
|
|
}, [title, currentSlug]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<aside className="space-y-4">
|
|
<h3 className="font-semibold text-foreground flex items-center gap-1.5">
|
|
Related articles
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Info className="w-3 h-3 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>This is currently in beta and may be inaccurate.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={i} className="animate-pulse">
|
|
<div className="h-4 bg-muted rounded w-3/4 mb-2"></div>
|
|
<div className="h-3 bg-muted rounded w-full"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
if (error || articles.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<aside className="space-y-4">
|
|
<h3 className="font-semibold text-foreground flex items-center gap-1.5">
|
|
Related articles
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Info className="w-3 h-3 text-muted-foreground cursor-help" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>This is currently in beta and may be inaccurate.</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</h3>
|
|
<ul className="space-y-3">
|
|
{articles.map((article) => {
|
|
const slug = article.title.replace(/ /g, '_').replace(/%20/g, '_');
|
|
return (
|
|
<li key={article.title}>
|
|
<Link
|
|
href={`/article/${slug}`}
|
|
className="group flex items-start gap-2 text-sm hover:opacity-80 transition-opacity"
|
|
>
|
|
<ArrowRight className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0 group-hover:text-blue-500 transition-colors" />
|
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">
|
|
{article.title}
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</aside>
|
|
);
|
|
}
|