Spaces:
Running
Running
| 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> | |
| ); | |
| } | |