Files
Tony0410 9025d1b8f1 Major feature update: Enhanced RSS, voice fixes, dark mode, and UI improvements
## 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>
2025-11-28 00:42:14 +00:00

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">&times;</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>
);
};