ArXivResearchExplorer / src /components /FavoritesView.tsx
RBJin's picture
Upload 20 files
81cb6e0 verified
import { useState, useEffect } from 'react';
import { Category, FavoritePaper } from '../types';
import {
getCategories,
addCategory,
removeCategory,
getFavorites,
removeFavorite,
moveFavoriteToCategory,
} from '../utils/storage';
interface Props {
refreshKey: number;
onViewPaper: (paperId: string) => void;
}
const COLORS = [
'#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f97316',
'#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6',
];
export default function FavoritesView({ refreshKey, onViewPaper }: Props) {
const [categories, setCategories] = useState<Category[]>([]);
const [favorites, setFavorites] = useState<FavoritePaper[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [showNewCategory, setShowNewCategory] = useState(false);
const [newCatName, setNewCatName] = useState('');
const [newCatColor, setNewCatColor] = useState(COLORS[0]);
const [movingPaper, setMovingPaper] = useState<string | null>(null);
const reload = () => {
setCategories(getCategories());
setFavorites(getFavorites());
};
useEffect(() => {
reload();
}, [refreshKey]);
const filteredFavorites = selectedCategory
? favorites.filter((f) => f.categoryId === selectedCategory)
: favorites;
const handleAddCategory = () => {
if (!newCatName.trim()) return;
addCategory(newCatName.trim(), newCatColor);
setNewCatName('');
setShowNewCategory(false);
reload();
};
const handleDeleteCategory = (id: string) => {
if (confirm('确定删除此分类?文章将移至未分类。\nDelete this category? Papers will be moved to Uncategorized.')) {
removeCategory(id);
if (selectedCategory === id) setSelectedCategory(null);
reload();
}
};
const handleRemoveFavorite = (paperId: string) => {
removeFavorite(paperId);
reload();
};
const handleMovePaper = (paperId: string, categoryId: string) => {
moveFavoriteToCategory(paperId, categoryId);
setMovingPaper(null);
reload();
};
const getCategoryName = (id: string) => {
const cat = categories.find((c) => c.id === id);
return cat?.name || '未分类';
};
const getCategoryColor = (id: string) => {
const cat = categories.find((c) => c.id === id);
return cat?.color || '#6b7280';
};
const getCategoryCount = (id: string) => {
return favorites.filter((f) => f.categoryId === id).length;
};
return (
<div className="max-w-6xl mx-auto">
<div className="flex gap-6">
{/* Sidebar - Categories */}
<div className="w-64 shrink-0">
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-700">📁 分类 Categories</h3>
<button
onClick={() => setShowNewCategory(!showNewCategory)}
className="text-sm text-indigo-600 hover:text-indigo-700 font-medium"
>
+ 新建
</button>
</div>
{showNewCategory && (
<div className="mb-4 p-3 bg-gray-50 rounded-lg space-y-2">
<input
type="text"
value={newCatName}
onChange={(e) => setNewCatName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddCategory()}
placeholder="分类名称 Category name"
className="w-full px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-indigo-500"
autoFocus
/>
<div className="flex flex-wrap gap-1">
{COLORS.map((c) => (
<button
key={c}
onClick={() => setNewCatColor(c)}
className={`w-6 h-6 rounded-full transition-transform ${
newCatColor === c ? 'ring-2 ring-offset-1 ring-indigo-400 scale-110' : ''
}`}
style={{ backgroundColor: c }}
/>
))}
</div>
<div className="flex gap-2">
<button
onClick={handleAddCategory}
className="flex-1 py-1 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
创建 Create
</button>
<button
onClick={() => setShowNewCategory(false)}
className="flex-1 py-1 text-sm bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300"
>
取消 Cancel
</button>
</div>
</div>
)}
{/* All favorites */}
<button
onClick={() => setSelectedCategory(null)}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors mb-1 ${
selectedCategory === null
? 'bg-indigo-50 text-indigo-700 font-medium'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
📋 全部收藏 All ({favorites.length})
</button>
{/* Category list */}
{categories.map((cat) => (
<div
key={cat.id}
className={`group flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors mb-1 cursor-pointer ${
selectedCategory === cat.id
? 'bg-indigo-50 text-indigo-700 font-medium'
: 'text-gray-600 hover:bg-gray-50'
}`}
onClick={() => setSelectedCategory(cat.id)}
>
<span
className="w-3 h-3 rounded-full shrink-0"
style={{ backgroundColor: cat.color }}
/>
<span className="flex-1 truncate">{cat.name}</span>
<span className="text-xs text-gray-400">{getCategoryCount(cat.id)}</span>
{cat.id !== 'uncategorized' && (
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCategory(cat.id);
}}
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 text-xs"
>
</button>
)}
</div>
))}
</div>
</div>
{/* Main - Favorite Papers */}
<div className="flex-1">
<h2 className="text-xl font-bold text-gray-800 mb-4">
⭐ {selectedCategory ? getCategoryName(selectedCategory) : '全部收藏 All Favorites'}
<span className="text-sm font-normal text-gray-400 ml-2">
({filteredFavorites.length} 篇)
</span>
</h2>
{filteredFavorites.length === 0 ? (
<div className="text-center py-20 text-gray-400">
<div className="text-5xl mb-4"></div>
<p>暂无收藏文章</p>
<p className="text-sm">No favorite papers yet</p>
<p className="text-sm mt-2">在搜索结果中点击 ☆ 收藏文章</p>
</div>
) : (
<div className="space-y-3">
{filteredFavorites.map((fav) => (
<div
key={fav.paperId}
className="bg-white rounded-xl border border-gray-100 shadow-sm p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-3">
<span
className="mt-1 w-3 h-3 rounded-full shrink-0"
style={{ backgroundColor: getCategoryColor(fav.categoryId) }}
title={getCategoryName(fav.categoryId)}
/>
<div className="flex-1 min-w-0">
<h4
className="font-medium text-gray-800 hover:text-indigo-600 cursor-pointer leading-snug"
onClick={() => onViewPaper(fav.paperId)}
>
{fav.paperTitle}
</h4>
<div className="mt-1 text-sm text-gray-500">
{fav.paperAuthors.slice(0, 3).join(', ')}
{fav.paperAuthors.length > 3 ? ` +${fav.paperAuthors.length - 3}` : ''}
</div>
<div className="mt-1 flex items-center gap-2 text-xs text-gray-400">
<span className="bg-gray-100 px-2 py-0.5 rounded">{fav.paperId}</span>
<span>{getCategoryName(fav.categoryId)}</span>
<span>·</span>
<span>{new Date(fav.timestamp).toLocaleDateString()}</span>
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0">
{/* Move to category */}
<div className="relative">
<button
onClick={() =>
setMovingPaper(movingPaper === fav.paperId ? null : fav.paperId)
}
className="p-1.5 text-xs bg-gray-50 text-gray-500 rounded-lg hover:bg-gray-100 transition-colors"
title="移动到分类 Move to category"
>
📁
</button>
{movingPaper === fav.paperId && (
<div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg py-1 z-10 min-w-[160px]">
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => handleMovePaper(fav.paperId, cat.id)}
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-gray-50 flex items-center gap-2 ${
fav.categoryId === cat.id ? 'font-medium text-indigo-600' : 'text-gray-600'
}`}
>
<span
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: cat.color }}
/>
{cat.name}
{fav.categoryId === cat.id && ' ✓'}
</button>
))}
</div>
)}
</div>
<a
href={`https://arxiv.org/abs/${fav.paperId}`}
target="_blank"
rel="noopener noreferrer"
className="p-1.5 text-xs bg-gray-50 text-gray-500 rounded-lg hover:bg-gray-100 transition-colors"
title="ArXiv"
>
🔗
</a>
<button
onClick={() => handleRemoveFavorite(fav.paperId)}
className="p-1.5 text-xs bg-red-50 text-red-400 rounded-lg hover:bg-red-100 hover:text-red-600 transition-colors"
title="取消收藏 Unfavorite"
>
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}