FeedItemDetail.tsx•7.95 kB
import { useEffect, useState } from 'react';
import { FeedItem } from '@/lib/db';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';
interface FeedItemDetailProps {
  itemId: string;
}
interface ArticleContent {
  id: string;
  url: string;
  title: string;
  content: string;
  html: string;
  author: string;
  published_date: string;
  image_url: string;
  summary: string;
  fetched_at: number;
}
export default function FeedItemDetail({ itemId }: FeedItemDetailProps) {
  const [item, setItem] = useState<FeedItem | null>(null);
  const [article, setArticle] = useState<ArticleContent | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [fetchingContent, setFetchingContent] = useState(false);
  useEffect(() => {
    const fetchItem = async () => {
      try {
        setLoading(true);
        setArticle(null);
        
        console.log('Fetching item with ID:', itemId);
        const response = await fetch(`/api/items?id=${encodeURIComponent(itemId)}`);
        console.log('fetchItem url', itemId, 'response:', response);
        if (!response.ok) {
          if (response.status === 404) {
            throw new Error('Item not found');
          }
          throw new Error('Failed to fetch item');
        }
        
        const data = await response.json();
        console.log('fetchItem data:', data);
        console.log('Item content:', data.content);
        console.log('Item summary:', data.summary);
        console.log('Item link:', data.link);
        setItem(data);
        setLoading(false);
        
        // After getting the item, fetch the full content only if a valid link is available
        if (data && data.link) {
          console.log('Calling fetchArticleContent with link:', data.link);
          fetchArticleContent(data.link);
        } else {
          console.log('No valid link available for item, cannot fetch article content');
          setFetchingContent(false);
        }
      } catch (err) {
        console.error('Error in fetchItem:', err);
        setError(err instanceof Error ? err.message : 'An error occurred');
        setLoading(false);
      }
    };
    
    fetchItem();
  }, [itemId]);
  
  // Function to fetch article content via firecrawl
  const fetchArticleContent = async (url: string) => {
    console.log('Fetching article content for:', url);
    
    try {
      setFetchingContent(true);
      
      // Check if we already have the article in the database
      console.log('Checking if article exists in database');
      const articleResponse = await fetch(`/api/articles?url=${encodeURIComponent(url)}`);
      console.log('Article response status:', articleResponse.status);
      
      if (articleResponse.ok) {
        const articleData = await articleResponse.json();
        console.log('Article data from database:', articleData);
        if (articleData && (articleData.html || articleData.content)) {
          console.log('Fetched article from database:', articleData);
          setArticle(articleData);
          setFetchingContent(false);
          return;
        }
      }
      
      // If article is not in the database, fetch it using the API
      console.log('Article not found in database, fetching from API');
      const fetchResponse = await fetch('/api/articles/fetch', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ url }),
      });
      
      if (fetchResponse.ok) {
        const fetchedArticle = await fetchResponse.json();
        console.log('Fetched article from API:', fetchedArticle);
        setArticle(fetchedArticle);
      } else {
        console.error('Failed to fetch article from API');
      }
      
      setFetchingContent(false);
    } catch (err) {
      console.error('Error fetching article content:', err);
      setFetchingContent(false);
    }
  };
  if (loading) {
    return (
      <div className="p-6 max-w-4xl mx-auto">
        <div className="animate-pulse">
          <div className="h-8 bg-gray-300 rounded w-3/4 mb-4"></div>
          <div className="h-4 bg-gray-300 rounded w-1/4 mb-6"></div>
          <div className="h-4 bg-gray-300 rounded w-full mb-2"></div>
          <div className="h-4 bg-gray-300 rounded w-full mb-2"></div>
          <div className="h-4 bg-gray-300 rounded w-full mb-2"></div>
          <div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
        </div>
      </div>
    );
  }
  if (error) {
    return (
      <div className="p-6 max-w-4xl mx-auto">
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
          <p>Error: {error}</p>
        </div>
      </div>
    );
  }
  if (!item) {
    return (
      <div className="p-6 max-w-4xl mx-auto">
        <div className="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded">
          <p>Item not found.</p>
        </div>
      </div>
    );
  }
  // Format the published date
  const formattedDate = new Date(item.published * 1000).toLocaleString();
  return (
    <div className="p-6 max-w-4xl mx-auto">
      <div className="mb-6">
        <h1 className="text-2xl font-bold mb-2">{item.title}</h1>
        
        <div className="flex justify-between items-center mb-4">
          <div className="text-gray-600">
            {item.feed_title && (
              <span>
                From: <Link href={`/feed/${encodeURIComponent(item.feed_id)}`} className="text-blue-600 hover:underline">
                  {item.feed_title}
                </Link>
              </span>
            )}
            {item.author && <span> • By {item.author}</span>}
          </div>
          <div className="text-gray-500">{formattedDate}</div>
        </div>
        
        {item.categories && item.categories.length > 0 && (
          <div className="flex gap-2 mb-4">
            {item.categories.map(category => (
              <span key={category} className="bg-gray-200 px-2 py-1 rounded text-sm">
                {category}
              </span>
            ))}
          </div>
        )}
        
        {item.link && (
          <div className="mb-6">
            <a 
              href={item.link} 
              target="_blank" 
              rel="noopener noreferrer"
              className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 inline-block"
            >
              Read Original Article
            </a>
          </div>
        )}
      </div>
      
      <div className="prose prose-lg max-w-none">
        {fetchingContent && (
          <div className="flex flex-col items-center justify-center p-6">
            <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
            <p className="text-lg text-gray-600">コンテンツをロード中</p>
          </div>
        )}
        
        {!fetchingContent && article && article.html ? (
          <div dangerouslySetInnerHTML={{ __html: article.html }} />
        ) : !fetchingContent && article && article.content ? (
          <ReactMarkdown>{article.content}</ReactMarkdown>
        ) : !fetchingContent && (item.content || item.summary) ? (
          <div dangerouslySetInnerHTML={{ __html: item.content || item.summary || '' }} />
        ) : !fetchingContent ? (
          <div className="text-center p-6">
            <p className="text-gray-600 mb-4">この記事のコンテンツは利用できません。</p>
            {item.link ? (
              <p className="text-gray-600">元の記事を読むには、上の「Read Original Article」ボタンをクリックしてください。</p>
            ) : (
              <p className="text-gray-600">この記事の元のURLは利用できません。</p>
            )}
          </div>
        ) : null}
      </div>
    </div>
  );
}