ملف آخر
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
هذا الالتزام موجود في:
193
src/ChannelView.tsx
Normal file
193
src/ChannelView.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useApp } from '../store';
|
||||
import { getChannel, getChannelPlaylists, getPlaylistVideos, standaloneVideos, formatNumber, getCategoryColor } from '../data';
|
||||
import { ArrowRight, Play, List, CheckCircle, Users, Video } from 'lucide-react';
|
||||
|
||||
export default function ChannelView({ channelId }: { channelId: string }) {
|
||||
const { setView, progress, followedChannelIds, followChannel, unfollowChannel } = useApp();
|
||||
const channel = getChannel(channelId);
|
||||
|
||||
if (!channel) {
|
||||
return <div className="p-8 text-center text-gray-400">القناة غير موجودة</div>;
|
||||
}
|
||||
|
||||
const isFollowed = followedChannelIds.includes(channelId);
|
||||
const channelPlaylists = getChannelPlaylists(channelId);
|
||||
const channelStandalone = standaloneVideos.filter(v => v.channelId === channelId);
|
||||
|
||||
const totalVideos = channelPlaylists.reduce((sum, pl) => sum + (getPlaylistVideos(pl.id)?.length || 0), 0) + channelStandalone.length;
|
||||
const watchedCount = [
|
||||
...channelStandalone,
|
||||
...channelPlaylists.flatMap(pl => getPlaylistVideos(pl.id) || [])
|
||||
].filter(v => progress.watchedVideos.includes(v.id)).length;
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-y-auto custom-scrollbar animate-slide-up">
|
||||
{/* Channel Header */}
|
||||
<div className={`bg-gradient-to-l ${channel.gradient} p-8 relative overflow-hidden`}>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIzMCIgY3k9IjMwIiByPSIxLjUiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wOCkiLz48L3N2Zz4=')] " />
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setView({ type: 'dashboard' })}
|
||||
className="flex items-center gap-1 text-white/80 hover:text-white text-sm mb-4 transition-colors"
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
العودة للرئيسية
|
||||
</button>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-20 h-20 rounded-2xl bg-white/20 backdrop-blur-sm flex items-center justify-center text-5xl shadow-2xl">
|
||||
{channel.emoji}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-black text-white mb-1">{channel.name}</h1>
|
||||
<p className="text-white/70 text-sm mb-3 max-w-lg">{channel.description}</p>
|
||||
<div className="flex items-center gap-4 text-white/60 text-xs">
|
||||
<span className="flex items-center gap-1"><Users className="w-3.5 h-3.5" /> {formatNumber(channel.subscriberCount)} مشترك</span>
|
||||
<span className="flex items-center gap-1"><Video className="w-3.5 h-3.5" /> {channel.videoCount} فيديو</span>
|
||||
<span className="flex items-center gap-1"><List className="w-3.5 h-3.5" /> {channelPlaylists.length} قائمة تشغيل</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => isFollowed ? unfollowChannel(channelId) : followChannel(channelId)}
|
||||
className={`px-6 py-2.5 rounded-xl font-bold text-sm transition-all ${
|
||||
isFollowed
|
||||
? 'bg-white/20 text-white hover:bg-white/30 backdrop-blur-sm'
|
||||
: 'bg-white text-gray-800 hover:bg-gray-100 shadow-lg'
|
||||
}`}
|
||||
>
|
||||
{isFollowed ? '✓ متابَع' : '+ متابعة'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex gap-2 mt-4 flex-wrap">
|
||||
{channel.categories.map(cat => (
|
||||
<span key={cat} className={`text-[10px] px-2.5 py-1 rounded-full ${getCategoryColor(cat)}`}>
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="px-6 py-4 flex gap-3">
|
||||
<div className="bg-white rounded-xl px-4 py-2.5 flex items-center gap-2 shadow-sm border border-gray-100">
|
||||
<div className="text-sm font-black text-orange-500">{watchedCount}/{totalVideos}</div>
|
||||
<div className="text-[10px] text-gray-400">فيديو مُشاهد</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl px-4 py-2.5 flex items-center gap-2 shadow-sm border border-gray-100">
|
||||
<div className="text-sm font-black text-emerald-500">
|
||||
{channelPlaylists.filter(pl => progress.completedPlaylists.includes(pl.id)).length}/{channelPlaylists.length}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-400">قوائم مكتملة</div>
|
||||
</div>
|
||||
{totalVideos > 0 && (
|
||||
<div className="flex-1 bg-white rounded-xl px-4 py-2.5 flex items-center gap-3 shadow-sm border border-gray-100">
|
||||
<div className="text-[10px] text-gray-400">التقدم الكلي</div>
|
||||
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-l from-orange-400 to-emerald-400 rounded-full" style={{ width: `${(watchedCount / totalVideos) * 100}%` }} />
|
||||
</div>
|
||||
<div className="text-xs font-bold text-gray-600">{Math.round((watchedCount / totalVideos) * 100)}%</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Playlists */}
|
||||
<div className="px-6 pb-6">
|
||||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2 mb-4">
|
||||
<List className="w-5 h-5 text-purple-500" />
|
||||
قوائم التشغيل ({channelPlaylists.length})
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
{channelPlaylists.map(pl => {
|
||||
const plVideos = getPlaylistVideos(pl.id);
|
||||
const completedCount = plVideos.filter(v => progress.watchedVideos.includes(v.id)).length;
|
||||
const percentage = plVideos.length > 0 ? Math.round((completedCount / plVideos.length) * 100) : 0;
|
||||
const isComplete = progress.completedPlaylists.includes(pl.id);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pl.id}
|
||||
onClick={() => setView({ type: 'playlist', playlistId: pl.id })}
|
||||
className="group bg-white rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden text-right border border-gray-100 hover:border-purple-200"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className={`h-28 bg-gradient-to-br ${pl.gradient} flex items-center justify-center`}>
|
||||
<span className="text-5xl opacity-40 group-hover:opacity-70 transition-opacity">{pl.emoji}</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="absolute bottom-2 right-3 left-3 flex justify-between items-end">
|
||||
<div className="flex items-center gap-1 bg-black/50 rounded-lg px-2 py-1 text-white text-[10px]">
|
||||
<Play className="w-3 h-3" />
|
||||
{plVideos.length} فيديو
|
||||
</div>
|
||||
{isComplete && (
|
||||
<div className="bg-emerald-500 rounded-lg px-2 py-1 text-white text-[10px] font-bold flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
مكتملة
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h4 className="font-bold text-gray-800 text-sm mb-1 group-hover:text-purple-600 transition-colors">{pl.title}</h4>
|
||||
<p className="text-[11px] text-gray-400 mb-3 line-clamp-2">{pl.description}</p>
|
||||
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ${percentage === 100 ? 'bg-emerald-400' : 'bg-gradient-to-l from-purple-400 to-pink-400'}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1.5 text-[10px]">
|
||||
<span className="text-gray-400">{completedCount}/{plVideos.length}</span>
|
||||
<span className={`font-bold ${percentage === 100 ? 'text-emerald-500' : 'text-purple-500'}`}>{percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Standalone Videos */}
|
||||
{channelStandalone.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2 mb-4">
|
||||
<Video className="w-5 h-5 text-orange-500" />
|
||||
فيديوهات حديثة
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{channelStandalone.map(video => {
|
||||
const isWatched = progress.watchedVideos.includes(video.id);
|
||||
return (
|
||||
<button
|
||||
key={video.id}
|
||||
onClick={() => {
|
||||
setView({ type: 'video', videoId: video.id });
|
||||
}}
|
||||
className="group bg-white rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden text-right border border-gray-100 hover:border-orange-200 flex items-center gap-3 p-3"
|
||||
>
|
||||
<div className={`w-24 h-16 rounded-xl bg-gradient-to-br ${channel.gradient} flex items-center justify-center shrink-0 relative`}>
|
||||
<Play className="w-6 h-6 text-white/70 group-hover:text-white transition-colors" />
|
||||
<span className="absolute bottom-1 left-1 bg-black/60 text-white text-[9px] px-1 rounded">
|
||||
{video.duration}
|
||||
</span>
|
||||
{isWatched && <CheckCircle className="w-3.5 h-3.5 text-emerald-400 absolute top-1 right-1" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-xs font-bold text-gray-800 line-clamp-2 group-hover:text-orange-600 transition-colors">{video.title}</h4>
|
||||
<div className="text-[10px] text-gray-400 mt-1">{formatNumber(video.viewCount)} مشاهدة</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم