mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 13:21:40 +08:00
## New Features - Article Summary Mode: AI-generated 30-second summaries with complexity analysis - Reading Stats Dashboard: Track articles read, listening time, and streaks - Bookmark/Resume: Auto-save progress when pausing, resume from where you left off - Audio Export: Export articles as downloadable WAV files - RSS Feed Manager: Subscribe to feeds with real-time validation and 31+ recommendations - Smart Speed: Auto-adjust playback based on article complexity - Voice Moods: Quick presets for different listening scenarios ## RSS Enhancements - Expanded recommendations from 8 to 31 sources across 5 categories: * General News (9 sources) * Technology (8 sources) * Business & Finance (5 sources) * Science & Research (5 sources) * International News (4 sources) - Real-time URL validation with visual feedback - Detailed error messages for different failure scenarios - Always-visible categorized recommendations - Auto-loading articles when feeds are added ## Bug Fixes - Fixed voice selection: Selected voice now consistently applies to playback - Implemented voice generation counter to prevent voice mixing between paragraphs - Fixed speed control to snap to clean 0.5 increments (1.0, 1.5, 2.0, etc.) - Fixed dark mode toggle by configuring Tailwind CDN for class-based dark mode - Removed vibe visualizer animation ## UI/UX Improvements - Redesigned voice selector with prominent voice panel and preview functionality - Added voice cards with emojis and descriptions - Enhanced feature toolbar with quick access to all new features - Improved reader view with better typography and reading modes - Added ambient reading modes (clean, sepia, night light) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
156 lines
6.5 KiB
TypeScript
156 lines
6.5 KiB
TypeScript
import React from 'react';
|
|
import { ReadingStats, VoiceName } from '../types';
|
|
import { BarChart3, Clock, BookOpen, Flame, Trophy, Mic } from 'lucide-react';
|
|
import { AVAILABLE_VOICES } from '../constants';
|
|
|
|
interface StatsPanelProps {
|
|
stats: ReadingStats;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const StatsPanel: React.FC<StatsPanelProps> = ({ stats, onClose }) => {
|
|
const formatTime = (minutes: number): string => {
|
|
if (minutes < 60) return `${Math.round(minutes)}m`;
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = Math.round(minutes % 60);
|
|
return `${hours}h ${mins}m`;
|
|
};
|
|
|
|
const favoriteVoiceData = AVAILABLE_VOICES.find(v => v.name === stats.favoriteVoice);
|
|
|
|
// Get last 7 days activity
|
|
const last7Days = Array.from({ length: 7 }, (_, i) => {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - (6 - i));
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
return {
|
|
day: date.toLocaleDateString('en', { weekday: 'short' }),
|
|
count: stats.articlesPerDay[dateStr] || 0
|
|
};
|
|
});
|
|
|
|
const maxCount = Math.max(...last7Days.map(d => d.count), 1);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
|
{/* Header */}
|
|
<div className="p-6 border-b border-slate-200 dark:border-slate-800">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
|
<BarChart3 className="w-6 h-6 text-blue-500" />
|
|
Reading Stats
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors"
|
|
>
|
|
<span className="text-slate-500 text-xl">×</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="p-6 space-y-6">
|
|
{/* Main Stats */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="bg-blue-50 dark:bg-blue-900/30 rounded-xl p-4">
|
|
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400 mb-1">
|
|
<BookOpen className="w-4 h-4" />
|
|
<span className="text-xs font-medium">Articles Read</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
{stats.totalArticlesRead}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-purple-50 dark:bg-purple-900/30 rounded-xl p-4">
|
|
<div className="flex items-center gap-2 text-purple-600 dark:text-purple-400 mb-1">
|
|
<Clock className="w-4 h-4" />
|
|
<span className="text-xs font-medium">Time Listened</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
{formatTime(stats.totalMinutesListened)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-orange-50 dark:bg-orange-900/30 rounded-xl p-4">
|
|
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400 mb-1">
|
|
<Flame className="w-4 h-4" />
|
|
<span className="text-xs font-medium">Current Streak</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
{stats.currentStreak} <span className="text-sm font-normal">days</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-green-50 dark:bg-green-900/30 rounded-xl p-4">
|
|
<div className="flex items-center gap-2 text-green-600 dark:text-green-400 mb-1">
|
|
<Trophy className="w-4 h-4" />
|
|
<span className="text-xs font-medium">Best Streak</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
{stats.longestStreak} <span className="text-sm font-normal">days</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Words Read */}
|
|
<div className="bg-slate-50 dark:bg-slate-800/50 rounded-xl p-4">
|
|
<p className="text-sm text-slate-500 dark:text-slate-400">Total Words Consumed</p>
|
|
<p className="text-3xl font-bold text-slate-900 dark:text-white">
|
|
{stats.totalWordsRead.toLocaleString()}
|
|
</p>
|
|
<p className="text-xs text-slate-400 mt-1">
|
|
That's about {Math.round(stats.totalWordsRead / 250)} pages!
|
|
</p>
|
|
</div>
|
|
|
|
{/* Weekly Activity */}
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
|
|
Last 7 Days
|
|
</h3>
|
|
<div className="flex items-end justify-between gap-2 h-24">
|
|
{last7Days.map((day, i) => (
|
|
<div key={i} className="flex-1 flex flex-col items-center gap-1">
|
|
<div className="w-full flex flex-col items-center">
|
|
<div
|
|
className="w-full bg-blue-500 dark:bg-blue-400 rounded-t transition-all"
|
|
style={{
|
|
height: `${Math.max((day.count / maxCount) * 60, 4)}px`,
|
|
opacity: day.count > 0 ? 1 : 0.2
|
|
}}
|
|
/>
|
|
</div>
|
|
<span className="text-xs text-slate-500 dark:text-slate-400">{day.day}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Favorite Voice */}
|
|
{favoriteVoiceData && (
|
|
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-900/30 dark:to-purple-900/30 rounded-xl p-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-3xl">{favoriteVoiceData.emoji}</span>
|
|
<div>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1">
|
|
<Mic className="w-3 h-3" /> Favorite Voice
|
|
</p>
|
|
<p className="font-semibold text-slate-900 dark:text-white">
|
|
{favoriteVoiceData.label}
|
|
</p>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
Used {stats.voiceUsage[stats.favoriteVoice] || 0} times
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|