wiki-project / src /components /ArticleViewer.tsx
Nagi15's picture
Add codebase
fcb5a67
import React, { useState } from 'react';
import { ArrowLeft, ExternalLink, BookOpen, Clock, Share2, Bookmark, Eye, Sparkles, Wand2, Loader2 } from 'lucide-react';
import { WikimediaAPI } from '../utils/wikimedia-api';
interface ArticleViewerProps {
title: string;
project: string;
content: string;
onBack: () => void;
onCreateStudyPlan?: (topic: string) => void;
onTransformContent?: (title: string, content: string) => void;
onViewArticle?: (title: string, project: string, content: string) => void;
}
const ArticleViewer: React.FC<ArticleViewerProps> = ({
title,
project,
content,
onBack,
onCreateStudyPlan,
onTransformContent,
onViewArticle
}) => {
const [fontSize, setFontSize] = useState('text-base');
const [readingTime, setReadingTime] = useState(0);
const [relatedArticles, setRelatedArticles] = useState<any[]>([]);
const [loadingRelated, setLoadingRelated] = useState(false);
const [showRelated, setShowRelated] = useState(false);
React.useEffect(() => {
// Calculate reading time (average 200 words per minute)
const wordCount = content.split(' ').length;
const time = Math.ceil(wordCount / 200);
setReadingTime(time);
}, [content]);
const formatContent = (text: string) => {
// Split content into paragraphs and format
const paragraphs = text.split('\n').filter(p => p.trim().length > 0);
return paragraphs.map((paragraph, index) => {
// Check if it's a heading (starts with ==)
if (paragraph.startsWith('==') && paragraph.endsWith('==')) {
const headingText = paragraph.replace(/=/g, '').trim();
return (
<h2 key={index} className="text-2xl font-bold text-gray-900 mt-8 mb-4 first:mt-0">
{headingText}
</h2>
);
}
// Regular paragraph
return (
<p key={index} className={`${fontSize} text-gray-700 leading-relaxed mb-4`}>
{paragraph}
</p>
);
});
};
const getProjectUrl = () => {
const baseUrls: Record<string, string> = {
wikipedia: 'https://en.wikipedia.org/wiki/',
'en-wikipedia': 'https://en.wikipedia.org/wiki/',
'es-wikipedia': 'https://es.wikipedia.org/wiki/',
'fr-wikipedia': 'https://fr.wikipedia.org/wiki/',
'de-wikipedia': 'https://de.wikipedia.org/wiki/',
wikibooks: 'https://en.wikibooks.org/wiki/',
wikiquote: 'https://en.wikiquote.org/wiki/',
wikiversity: 'https://en.wikiversity.org/wiki/',
wiktionary: 'https://en.wiktionary.org/wiki/',
wikisource: 'https://en.wikisource.org/wiki/',
};
const baseUrl = baseUrls[project] || baseUrls.wikipedia;
return baseUrl + encodeURIComponent(title.replace(/ /g, '_'));
};
const handleShare = async () => {
if (navigator.share) {
try {
await navigator.share({
title: title,
text: `Check out this article about ${title}`,
url: window.location.href,
});
} catch (error) {
console.log('Error sharing:', error);
}
} else {
// Fallback: copy to clipboard
navigator.clipboard.writeText(window.location.href);
}
};
const handleCreateStudyPlan = () => {
if (onCreateStudyPlan) {
onCreateStudyPlan(title);
}
};
const handleTransformContent = () => {
if (onTransformContent) {
onTransformContent(title, content);
}
};
const handleFindRelated = async () => {
setLoadingRelated(true);
setShowRelated(true);
try {
// Search for related articles using keywords from the title
const searchTerms = title.split(' ').filter(term => term.length > 3);
const relatedResults = [];
// Search for each term and collect results
for (const term of searchTerms.slice(0, 2)) {
const results = await WikimediaAPI.search(term, 'wikipedia', 3);
relatedResults.push(...results.filter(r => r.title !== title));
}
// Remove duplicates and limit to 6 results
const uniqueResults = relatedResults.filter((result, index, self) =>
index === self.findIndex(r => r.title === result.title)
).slice(0, 6);
setRelatedArticles(uniqueResults);
} catch (error) {
console.error('Failed to find related articles:', error);
setRelatedArticles([]);
} finally {
setLoadingRelated(false);
}
};
const handleViewRelated = async (article: any) => {
try {
const content = await WikimediaAPI.getPageContent(article.title, article.project);
if (onViewArticle) {
onViewArticle(article.title, article.project, content);
}
} catch (error) {
console.error('Failed to load related article:', error);
}
};
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<button
onClick={onBack}
className="flex items-center space-x-2 text-primary-600 hover:text-primary-700 font-medium mb-6 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span>Back</span>
</button>
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<span className="px-3 py-1 bg-primary-100 text-primary-700 text-sm rounded-full font-medium capitalize">
{project.replace('-wikipedia', '')}
</span>
<div className="flex items-center text-sm text-gray-600">
<Clock className="w-4 h-4 mr-1" />
<span>{readingTime} min read</span>
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">{title}</h1>
</div>
</div>
{/* Article Controls */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-600">Font size:</span>
<select
value={fontSize}
onChange={(e) => setFontSize(e.target.value)}
className="text-sm border border-gray-300 rounded px-2 py-1"
>
<option value="text-sm">Small</option>
<option value="text-base">Medium</option>
<option value="text-lg">Large</option>
<option value="text-xl">Extra Large</option>
</select>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={handleShare}
className="flex items-center space-x-1 px-3 py-2 text-gray-600 hover:text-gray-900 transition-colors"
>
<Share2 className="w-4 h-4" />
<span className="text-sm">Share</span>
</button>
<button className="flex items-center space-x-1 px-3 py-2 text-gray-600 hover:text-gray-900 transition-colors">
<Bookmark className="w-4 h-4" />
<span className="text-sm">Save</span>
</button>
<a
href={getProjectUrl()}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-1 px-3 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
<ExternalLink className="w-4 h-4" />
<span className="text-sm">View Original</span>
</a>
</div>
</div>
</div>
</div>
{/* Article Content */}
<div className="bg-white rounded-2xl p-8 border border-gray-200 shadow-sm">
<div className="prose max-w-none">
{formatContent(content)}
</div>
{content.length < 500 && (
<div className="mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-yellow-800 text-sm">
This appears to be a short excerpt. For the complete article with images, references, and full content,
please visit the original source.
</p>
</div>
)}
</div>
{/* Related Actions */}
<div className="mt-8 bg-white rounded-2xl p-6 border border-gray-200 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 mb-4">What's Next?</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={handleCreateStudyPlan}
className="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors group"
>
<Sparkles className="w-6 h-6 text-primary-600 group-hover:scale-110 transition-transform" />
<div className="text-left">
<div className="font-medium text-gray-900">Create Study Plan</div>
<div className="text-sm text-gray-600">Generate a learning path from this topic</div>
</div>
</button>
<button
onClick={handleFindRelated}
disabled={loadingRelated}
className="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors group disabled:opacity-50"
>
{loadingRelated ? (
<Loader2 className="w-6 h-6 text-primary-600 animate-spin" />
) : (
<Eye className="w-6 h-6 text-primary-600 group-hover:scale-110 transition-transform" />
)}
<div className="text-left">
<div className="font-medium text-gray-900">Find Related</div>
<div className="text-sm text-gray-600">
{loadingRelated ? 'Searching...' : 'Discover similar articles'}
</div>
</div>
</button>
<button
onClick={handleTransformContent}
className="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors group"
>
<Wand2 className="w-6 h-6 text-primary-600 group-hover:scale-110 transition-transform" />
<div className="text-left">
<div className="font-medium text-gray-900">Transform Content</div>
<div className="text-sm text-gray-600">Create quiz or summary</div>
</div>
</button>
</div>
</div>
{/* Related Articles Section */}
{showRelated && (
<div className="mt-8 bg-white rounded-2xl p-6 border border-gray-200 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Related Articles</h3>
{loadingRelated ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-primary-600" />
<span className="ml-2 text-gray-600">Finding related articles...</span>
</div>
) : relatedArticles.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{relatedArticles.map((article, index) => (
<div
key={index}
className="p-4 border border-gray-200 rounded-lg hover:border-primary-200 hover:bg-primary-50 transition-colors cursor-pointer"
onClick={() => handleViewRelated(article)}
>
<h4 className="font-medium text-gray-900 mb-2">{article.title}</h4>
<p className="text-sm text-gray-600 mb-2">
{article.snippet.replace(/<[^>]*>/g, '').substring(0, 100)}...
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-primary-600 bg-primary-100 px-2 py-1 rounded-full">
{article.project}
</span>
<button className="text-sm text-primary-600 hover:text-primary-700">
Read more →
</button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<p className="text-gray-600">No related articles found. Try searching for specific topics.</p>
</div>
)}
</div>
)}
</div>
);
};
export default ArticleViewer;