2025-06-28 19:30:35 -03:00
|
|
|
|
"use client";
|
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Search, Filter, Grid, List, Play, Pause, Volume2, VolumeX,
|
|
|
|
|
Eye, Bookmark, Share, Download, Clock, TrendingUp,
|
|
|
|
|
Globe, Calendar, Tag, ExternalLink, MoreHorizontal,
|
|
|
|
|
ChevronLeft, ChevronRight, Heart, MessageCircle,
|
|
|
|
|
Video, Image, Music, FileText, Rss, Archive,
|
|
|
|
|
Star, ThumbsUp, Settings, RefreshCw, Maximize,
|
|
|
|
|
User, MapPin, Zap, Briefcase, Gamepad2, Palette,
|
|
|
|
|
Users, Award, ShoppingBag, Plane, DollarSign,
|
|
|
|
|
Newspaper, AlertTriangle, Target, Mic, Camera
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-04-27 15:25:45 -03:00
|
|
|
|
|
2025-06-28 19:30:35 -03:00
|
|
|
|
// Mock news data - realistic news structure
|
|
|
|
|
const mockNews = [
|
|
|
|
|
{
|
|
|
|
|
id: 1,
|
|
|
|
|
headline: "Major Climate Summit Reaches Breakthrough Agreement on Carbon Reduction",
|
|
|
|
|
summary: "World leaders agree to unprecedented measures targeting 50% reduction in emissions by 2030, with binding commitments from major economies.",
|
|
|
|
|
content: "In a historic development at the Global Climate Summit, representatives from 195 countries have agreed to a comprehensive framework...",
|
|
|
|
|
author: "Sarah Chen",
|
|
|
|
|
publication: "Global Times",
|
|
|
|
|
publishedAt: "2025-01-15T14:30:00Z",
|
|
|
|
|
category: "Environment",
|
|
|
|
|
subcategory: "Climate Change",
|
|
|
|
|
readTime: "8 min read",
|
|
|
|
|
mediaType: "article",
|
|
|
|
|
imageUrl: "/api/placeholder/800/400",
|
|
|
|
|
videoUrl: null,
|
|
|
|
|
audioUrl: null,
|
|
|
|
|
tags: ["climate", "politics", "international", "environment"],
|
|
|
|
|
location: "Geneva, Switzerland",
|
|
|
|
|
breaking: true,
|
|
|
|
|
featured: true,
|
|
|
|
|
viewCount: 45670,
|
|
|
|
|
shareCount: 1240,
|
|
|
|
|
commentCount: 892,
|
|
|
|
|
bookmarkCount: 2130,
|
|
|
|
|
sentiment: "positive",
|
|
|
|
|
credibilityScore: 0.94
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 2,
|
|
|
|
|
headline: "Tech Giant Announces Revolutionary AI Chip Architecture",
|
|
|
|
|
summary: "New quantum-enhanced processing unit promises 1000x performance improvement for AI workloads, reshaping the semiconductor industry.",
|
|
|
|
|
content: "Silicon Valley's leading tech company unveiled its latest innovation today, a groundbreaking AI chip that combines...",
|
|
|
|
|
author: "Marcus Rodriguez",
|
|
|
|
|
publication: "Tech Weekly",
|
|
|
|
|
publishedAt: "2025-01-15T12:45:00Z",
|
|
|
|
|
category: "Technology",
|
|
|
|
|
subcategory: "AI & Computing",
|
|
|
|
|
readTime: "6 min read",
|
|
|
|
|
mediaType: "video",
|
|
|
|
|
imageUrl: "/api/placeholder/800/400",
|
|
|
|
|
videoUrl: "/api/placeholder/video",
|
|
|
|
|
audioUrl: null,
|
|
|
|
|
videoDuration: "15:32",
|
|
|
|
|
tags: ["ai", "technology", "semiconductors", "innovation"],
|
|
|
|
|
location: "San Francisco, CA",
|
|
|
|
|
breaking: false,
|
|
|
|
|
featured: true,
|
|
|
|
|
viewCount: 78234,
|
|
|
|
|
shareCount: 2156,
|
|
|
|
|
commentCount: 445,
|
|
|
|
|
bookmarkCount: 3421,
|
|
|
|
|
sentiment: "positive",
|
|
|
|
|
credibilityScore: 0.91
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 3,
|
|
|
|
|
headline: "Global Markets React to Federal Reserve Interest Rate Decision",
|
|
|
|
|
summary: "Stock markets surge as Fed maintains current rates, signaling confidence in economic recovery and inflation control measures.",
|
|
|
|
|
content: "Financial markets around the world responded positively to the Federal Reserve's decision to maintain...",
|
|
|
|
|
author: "Elena Vasquez",
|
|
|
|
|
publication: "Financial Herald",
|
|
|
|
|
publishedAt: "2025-01-15T11:20:00Z",
|
|
|
|
|
category: "Business",
|
|
|
|
|
subcategory: "Markets",
|
|
|
|
|
readTime: "5 min read",
|
|
|
|
|
mediaType: "article",
|
|
|
|
|
imageUrl: "/api/placeholder/800/400",
|
|
|
|
|
videoUrl: null,
|
|
|
|
|
audioUrl: null,
|
|
|
|
|
tags: ["finance", "fed", "markets", "economy"],
|
|
|
|
|
location: "New York, NY",
|
|
|
|
|
breaking: false,
|
|
|
|
|
featured: false,
|
|
|
|
|
viewCount: 34567,
|
|
|
|
|
shareCount: 876,
|
|
|
|
|
commentCount: 234,
|
|
|
|
|
bookmarkCount: 1456,
|
|
|
|
|
sentiment: "neutral",
|
|
|
|
|
credibilityScore: 0.96
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 4,
|
|
|
|
|
headline: "Space Mission Discovers Evidence of Ancient Water on Mars",
|
|
|
|
|
summary: "NASA's latest rover findings reveal extensive underground water systems, raising new questions about potential for past life.",
|
|
|
|
|
content: "The Mars exploration mission has yielded its most significant discovery yet, with clear evidence of...",
|
|
|
|
|
author: "Dr. James Mitchell",
|
|
|
|
|
publication: "Science Today",
|
|
|
|
|
publishedAt: "2025-01-15T09:15:00Z",
|
|
|
|
|
category: "Science",
|
|
|
|
|
subcategory: "Space",
|
|
|
|
|
readTime: "10 min read",
|
|
|
|
|
mediaType: "multimedia",
|
|
|
|
|
imageUrl: "/api/placeholder/800/400",
|
|
|
|
|
videoUrl: "/api/placeholder/video",
|
|
|
|
|
audioUrl: "/api/placeholder/audio",
|
|
|
|
|
videoDuration: "8:45",
|
|
|
|
|
audioDuration: "12:30",
|
|
|
|
|
tags: ["space", "mars", "nasa", "discovery"],
|
|
|
|
|
location: "Pasadena, CA",
|
|
|
|
|
breaking: false,
|
|
|
|
|
featured: true,
|
|
|
|
|
viewCount: 92341,
|
|
|
|
|
shareCount: 4567,
|
|
|
|
|
commentCount: 1234,
|
|
|
|
|
bookmarkCount: 5678,
|
|
|
|
|
sentiment: "positive",
|
|
|
|
|
credibilityScore: 0.98
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 5,
|
|
|
|
|
headline: "Championship Final Breaks Viewership Records Worldwide",
|
|
|
|
|
summary: "Historic match draws over 2 billion viewers globally, showcasing the growing international appeal of the sport.",
|
|
|
|
|
content: "Last night's championship final has set a new benchmark for global sports viewership...",
|
|
|
|
|
author: "Alex Thompson",
|
|
|
|
|
publication: "Sports Central",
|
|
|
|
|
publishedAt: "2025-01-15T08:30:00Z",
|
|
|
|
|
category: "Sports",
|
|
|
|
|
subcategory: "Football",
|
|
|
|
|
readTime: "4 min read",
|
|
|
|
|
mediaType: "video",
|
|
|
|
|
imageUrl: "/api/placeholder/800/400",
|
|
|
|
|
videoUrl: "/api/placeholder/video",
|
|
|
|
|
audioUrl: null,
|
|
|
|
|
videoDuration: "22:15",
|
|
|
|
|
tags: ["sports", "football", "championship", "records"],
|
|
|
|
|
location: "London, UK",
|
|
|
|
|
breaking: false,
|
|
|
|
|
featured: false,
|
|
|
|
|
viewCount: 156789,
|
|
|
|
|
shareCount: 8901,
|
|
|
|
|
commentCount: 3456,
|
|
|
|
|
bookmarkCount: 2345,
|
|
|
|
|
sentiment: "positive",
|
|
|
|
|
credibilityScore: 0.89
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const categories = [
|
|
|
|
|
{ id: 'all', name: 'All News', icon: Newspaper, color: 'text-gray-600' },
|
|
|
|
|
{ id: 'breaking', name: 'Breaking', icon: AlertTriangle, color: 'text-red-600' },
|
|
|
|
|
{ id: 'politics', name: 'Politics', icon: Users, color: 'text-blue-600' },
|
|
|
|
|
{ id: 'technology', name: 'Technology', icon: Zap, color: 'text-purple-600' },
|
|
|
|
|
{ id: 'business', name: 'Business', icon: Briefcase, color: 'text-green-600' },
|
|
|
|
|
{ id: 'science', name: 'Science', icon: Target, color: 'text-indigo-600' },
|
|
|
|
|
{ id: 'sports', name: 'Sports', icon: Award, color: 'text-orange-600' },
|
|
|
|
|
{ id: 'entertainment', name: 'Entertainment', icon: Palette, color: 'text-pink-600' },
|
|
|
|
|
{ id: 'health', name: 'Health', icon: Heart, color: 'text-emerald-600' },
|
|
|
|
|
{ id: 'world', name: 'World', icon: Globe, color: 'text-cyan-600' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateString) => {
|
|
|
|
|
const date = new Date(dateString);
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const diffMs = now - date;
|
|
|
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
|
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
|
|
|
|
|
|
if (diffHours < 1) return 'Just now';
|
|
|
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
|
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
|
|
|
return date.toLocaleDateString();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatNumber = (num) => {
|
|
|
|
|
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
|
|
|
|
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
|
|
|
|
return num.toString();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const MediaPlayer = ({ article, onClose }) => {
|
|
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
|
const [isMuted, setIsMuted] = useState(false);
|
|
|
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
|
|
|
const [duration, setDuration] = useState(0);
|
|
|
|
|
const videoRef = useRef(null);
|
|
|
|
|
const audioRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
const togglePlay = () => {
|
|
|
|
|
if (article.mediaType === 'video' && videoRef.current) {
|
|
|
|
|
if (isPlaying) {
|
|
|
|
|
videoRef.current.pause();
|
|
|
|
|
} else {
|
|
|
|
|
videoRef.current.play();
|
|
|
|
|
}
|
|
|
|
|
setIsPlaying(!isPlaying);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleMute = () => {
|
|
|
|
|
if (videoRef.current) {
|
|
|
|
|
videoRef.current.muted = !isMuted;
|
|
|
|
|
setIsMuted(!isMuted);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4">
|
|
|
|
|
<div className="bg-background rounded-xl w-full max-w-4xl max-h-[90vh] overflow-hidden">
|
|
|
|
|
<div className="flex items-center justify-between p-4 border-b">
|
|
|
|
|
<h2 className="text-lg font-semibold truncate">{article.headline}</h2>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
|
|
|
|
>
|
|
|
|
|
×
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
{article.mediaType === 'video' && (
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<video
|
|
|
|
|
ref={videoRef}
|
|
|
|
|
className="w-full h-auto max-h-[60vh]"
|
|
|
|
|
poster={article.imageUrl}
|
|
|
|
|
onTimeUpdate={(e) => setCurrentTime(e.target.currentTime)}
|
|
|
|
|
onLoadedMetadata={(e) => setDuration(e.target.duration)}
|
|
|
|
|
>
|
|
|
|
|
<source src={article.videoUrl} type="video/mp4" />
|
|
|
|
|
</video>
|
|
|
|
|
|
|
|
|
|
<div className="absolute bottom-4 left-4 flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={togglePlay}
|
|
|
|
|
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={toggleMute}
|
|
|
|
|
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{isMuted ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
|
|
|
|
|
</button>
|
|
|
|
|
<span className="text-white text-sm bg-black/50 px-2 py-1 rounded">
|
|
|
|
|
{article.videoDuration}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{article.mediaType === 'audio' && (
|
|
|
|
|
<div className="p-8 text-center">
|
|
|
|
|
<div className="w-32 h-32 mx-auto mb-4 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
|
|
|
|
|
<Music className="w-16 h-16 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<audio
|
|
|
|
|
ref={audioRef}
|
|
|
|
|
controls
|
|
|
|
|
className="w-full max-w-md mx-auto"
|
|
|
|
|
src={article.audioUrl}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
<div className="text-sm text-muted-foreground mb-2">
|
|
|
|
|
{article.author} • {article.publication} • {formatDate(article.publishedAt)}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm leading-relaxed">{article.summary}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const NewsCard = ({ article, layout = 'grid', onPlay, onBookmark, onShare }) => {
|
|
|
|
|
const getMediaIcon = () => {
|
|
|
|
|
switch (article.mediaType) {
|
|
|
|
|
case 'video': return <Video className="w-4 h-4" />;
|
|
|
|
|
case 'audio': return <Music className="w-4 h-4" />;
|
|
|
|
|
case 'multimedia': return <Camera className="w-4 h-4" />;
|
|
|
|
|
default: return <FileText className="w-4 h-4" />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getSentimentColor = () => {
|
|
|
|
|
switch (article.sentiment) {
|
|
|
|
|
case 'positive': return 'text-green-600';
|
|
|
|
|
case 'negative': return 'text-red-600';
|
|
|
|
|
default: return 'text-gray-600';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (layout === 'list') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex gap-4 p-4 border-b hover:bg-muted/50 transition-colors">
|
|
|
|
|
<div className="w-32 h-24 bg-muted rounded-lg overflow-hidden flex-shrink-0">
|
|
|
|
|
<img src={article.imageUrl} alt="" className="w-full h-full object-cover" />
|
|
|
|
|
{(article.mediaType === 'video' || article.mediaType === 'multimedia') && (
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onPlay(article)}
|
|
|
|
|
className="p-2 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Play className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-start justify-between gap-2">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
|
|
|
|
{article.breaking && (
|
|
|
|
|
<span className="bg-red-600 text-white text-xs px-2 py-1 rounded-full font-medium">
|
|
|
|
|
BREAKING
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
|
|
|
|
|
{article.category}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{getMediaIcon()}
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{article.videoDuration || article.audioDuration || article.readTime}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h3 className="font-semibold text-lg mb-2 line-clamp-2 hover:text-blue-600 cursor-pointer">
|
|
|
|
|
{article.headline}
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-2 line-clamp-2">
|
|
|
|
|
{article.summary}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
|
|
|
<span className="font-medium">{article.author}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{article.publication}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{formatDate(article.publishedAt)}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<Eye className="w-3 h-3" />
|
|
|
|
|
{formatNumber(article.viewCount)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onBookmark(article)}
|
|
|
|
|
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Bookmark className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onShare(article)}
|
|
|
|
|
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Share className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-card border rounded-xl overflow-hidden hover:shadow-lg transition-all duration-300 group">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<img
|
|
|
|
|
src={article.imageUrl}
|
|
|
|
|
alt={article.headline}
|
|
|
|
|
className="w-full h-48 object-cover"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{(article.mediaType === 'video' || article.mediaType === 'multimedia') && (
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onPlay(article)}
|
|
|
|
|
className="p-4 bg-black/50 text-white rounded-full hover:bg-black/70 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Play className="w-6 h-6" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="absolute top-3 left-3 flex items-center gap-2">
|
|
|
|
|
{article.breaking && (
|
|
|
|
|
<span className="bg-red-600 text-white text-xs px-2 py-1 rounded-full font-medium">
|
|
|
|
|
BREAKING
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<span className="bg-black/70 text-white text-xs px-2 py-1 rounded-full">
|
|
|
|
|
{article.category}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="absolute top-3 right-3 flex items-center gap-1">
|
|
|
|
|
{getMediaIcon()}
|
|
|
|
|
<span className="text-white text-xs bg-black/70 px-2 py-1 rounded-full">
|
|
|
|
|
{article.videoDuration || article.audioDuration || article.readTime}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
<h3 className="font-semibold text-lg mb-2 line-clamp-2 hover:text-blue-600 cursor-pointer">
|
|
|
|
|
{article.headline}
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-3 line-clamp-3">
|
|
|
|
|
{article.summary}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="font-medium">{article.author}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{formatDate(article.publishedAt)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Eye className="w-3 h-3" />
|
|
|
|
|
{formatNumber(article.viewCount)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<Heart className="w-3 h-3" />
|
|
|
|
|
{formatNumber(article.shareCount)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<MessageCircle className="w-3 h-3" />
|
|
|
|
|
{formatNumber(article.commentCount)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onBookmark(article)}
|
|
|
|
|
className="p-1 hover:bg-muted rounded transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Bookmark className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onShare(article)}
|
|
|
|
|
className="p-1 hover:bg-muted rounded transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Share className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button className="p-1 hover:bg-muted rounded transition-colors">
|
|
|
|
|
<MoreHorizontal className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function NewsMediaBrowser() {
|
|
|
|
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
const [layout, setLayout] = useState('grid');
|
|
|
|
|
const [sortBy, setSortBy] = useState('latest');
|
|
|
|
|
const [articles, setArticles] = useState(mockNews);
|
|
|
|
|
const [selectedArticle, setSelectedArticle] = useState(null);
|
|
|
|
|
const [showPlayer, setShowPlayer] = useState(false);
|
|
|
|
|
const [bookmarkedArticles, setBookmarkedArticles] = useState(new Set());
|
|
|
|
|
|
|
|
|
|
const filteredArticles = articles.filter(article => {
|
|
|
|
|
const matchesCategory = selectedCategory === 'all' ||
|
|
|
|
|
article.category.toLowerCase() === selectedCategory ||
|
|
|
|
|
(selectedCategory === 'breaking' && article.breaking);
|
|
|
|
|
|
|
|
|
|
const matchesSearch = searchTerm === '' ||
|
|
|
|
|
article.headline.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
article.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
|
|
|
|
|
|
|
|
return matchesCategory && matchesSearch;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sortedArticles = [...filteredArticles].sort((a, b) => {
|
|
|
|
|
switch (sortBy) {
|
|
|
|
|
case 'latest':
|
|
|
|
|
return new Date(b.publishedAt) - new Date(a.publishedAt);
|
|
|
|
|
case 'popular':
|
|
|
|
|
return b.viewCount - a.viewCount;
|
|
|
|
|
case 'trending':
|
|
|
|
|
return b.shareCount - a.shareCount;
|
|
|
|
|
default:
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handlePlay = (article) => {
|
|
|
|
|
setSelectedArticle(article);
|
|
|
|
|
setShowPlayer(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBookmark = (article) => {
|
|
|
|
|
const newBookmarked = new Set(bookmarkedArticles);
|
|
|
|
|
if (newBookmarked.has(article.id)) {
|
|
|
|
|
newBookmarked.delete(article.id);
|
|
|
|
|
} else {
|
|
|
|
|
newBookmarked.add(article.id);
|
|
|
|
|
}
|
|
|
|
|
setBookmarkedArticles(newBookmarked);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleShare = (article) => {
|
|
|
|
|
navigator.share?.({
|
|
|
|
|
title: article.headline,
|
|
|
|
|
text: article.summary,
|
|
|
|
|
url: window.location.href + '/' + article.id
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-screen bg-background">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="border-b bg-background/95 backdrop-blur-sm sticky top-0 z-40">
|
|
|
|
|
<div className="px-6 py-4">
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
|
|
|
|
News Center
|
|
|
|
|
</h1>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button className="flex items-center gap-2 px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
|
|
|
|
<RefreshCw className="w-4 h-4" />
|
|
|
|
|
Refresh
|
|
|
|
|
</button>
|
|
|
|
|
<button className="p-2 hover:bg-muted rounded-lg transition-colors">
|
|
|
|
|
<Settings className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search and Filters */}
|
|
|
|
|
<div className="flex items-center gap-4 mb-4">
|
|
|
|
|
<div className="relative flex-1 max-w-md">
|
|
|
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search news..."
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
value={sortBy}
|
|
|
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
|
|
|
className="px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
>
|
|
|
|
|
<option value="latest">Latest</option>
|
|
|
|
|
<option value="popular">Most Popular</option>
|
|
|
|
|
<option value="trending">Trending</option>
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setLayout('grid')}
|
|
|
|
|
className={cn(
|
|
|
|
|
"p-2 rounded transition-colors",
|
|
|
|
|
layout === 'grid' ? "bg-background shadow-sm" : "hover:bg-background/50"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Grid className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setLayout('list')}
|
|
|
|
|
className={cn(
|
|
|
|
|
"p-2 rounded transition-colors",
|
|
|
|
|
layout === 'list' ? "bg-background shadow-sm" : "hover:bg-background/50"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<List className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Categories */}
|
|
|
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
|
|
|
{categories.map((category) => (
|
|
|
|
|
<button
|
|
|
|
|
key={category.id}
|
|
|
|
|
onClick={() => setSelectedCategory(category.id)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 px-4 py-2 rounded-full whitespace-nowrap transition-all",
|
|
|
|
|
selectedCategory === category.id
|
|
|
|
|
? "bg-blue-600 text-white"
|
|
|
|
|
: "bg-muted hover:bg-muted/80"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<category.icon className={cn("w-4 h-4", category.color)} />
|
|
|
|
|
{category.name}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="flex-1 overflow-auto p-6">
|
|
|
|
|
<div className="max-w-7xl mx-auto">
|
|
|
|
|
{/* Featured Articles */}
|
|
|
|
|
{selectedCategory === 'all' && (
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<h2 className="text-xl font-semibold mb-4">Featured Stories</h2>
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
|
{sortedArticles.filter(article => article.featured).slice(0, 2).map((article) => (
|
|
|
|
|
<NewsCard
|
|
|
|
|
key={article.id}
|
|
|
|
|
article={article}
|
|
|
|
|
layout="grid"
|
|
|
|
|
onPlay={handlePlay}
|
|
|
|
|
onBookmark={handleBookmark}
|
|
|
|
|
onShare={handleShare}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Article Grid/List */}
|
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
|
|
|
|
<h2 className="text-xl font-semibold">
|
|
|
|
|
{selectedCategory === 'all' ? 'All News' : categories.find(c => c.id === selectedCategory)?.name}
|
|
|
|
|
</h2>
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
{sortedArticles.length} articles
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{layout === 'grid' ? (
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
{sortedArticles.map((article) => (
|
|
|
|
|
<NewsCard
|
|
|
|
|
key={article.id}
|
|
|
|
|
article={article}
|
|
|
|
|
layout="grid"
|
|
|
|
|
onPlay={handlePlay}
|
|
|
|
|
onBookmark={handleBookmark}
|
|
|
|
|
onShare={handleShare}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-0 border rounded-xl overflow-hidden">
|
|
|
|
|
{sortedArticles.map((article) => (
|
|
|
|
|
<NewsCard
|
|
|
|
|
key={article.id}
|
|
|
|
|
article={article}
|
|
|
|
|
layout="list"
|
|
|
|
|
onPlay={handlePlay}
|
|
|
|
|
onBookmark={handleBookmark}
|
|
|
|
|
onShare={handleShare}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Media Player Modal */}
|
|
|
|
|
{showPlayer && selectedArticle && (
|
|
|
|
|
<MediaPlayer
|
|
|
|
|
article={selectedArticle}
|
|
|
|
|
onClose={() => setShowPlayer(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-04-27 15:25:45 -03:00
|
|
|
|
}
|