gbclient/app/media/page.tsx
Rodrigo Rodriguez (Pragmatismo) 2a7aa20c4d
Some checks failed
GBCI / build (push) Failing after 10m45s
Add new themes: Orange and XTree Gold
- Introduced a new CSS theme for Orange, featuring a modern color palette with distinct foreground and background colors.
- Added an XTree Gold theme that emulates the classic 1980s DOS interface, complete with authentic colors and styles for file management elements.
- Both themes include variables for customization and specific styles for various UI components such as cards, popovers, and menus.
2025-06-28 19:30:35 -03:00

692 lines
No EOL
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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