Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
298 أسطر
16 KiB
TypeScript
298 أسطر
16 KiB
TypeScript
import { useApp } from '../store';
|
||
import { channels, playlists, videos, standaloneVideos, formatNumber, getChannel, getPlaylistVideos } from '../data';
|
||
import { Play, Clock, CheckCircle, Trophy, Flame, Star, BookOpen } from 'lucide-react';
|
||
|
||
export default function Dashboard() {
|
||
const { progress, setView, followedChannelIds, markVideoWatched } = useApp();
|
||
|
||
const followedChannels = channels.filter(c => followedChannelIds.includes(c.id));
|
||
const followedPlaylists = playlists.filter(p => followedChannelIds.includes(p.channelId));
|
||
|
||
// Get playlists with progress
|
||
const playlistsInProgress = followedPlaylists
|
||
.map(pl => ({
|
||
playlist: pl,
|
||
channel: getChannel(pl.channelId),
|
||
plVideos: getPlaylistVideos(pl.id),
|
||
currentIndex: progress.playlistProgress[pl.id] || 0,
|
||
completedCount: getPlaylistVideos(pl.id).filter(v => progress.watchedVideos.includes(v.id)).length,
|
||
}))
|
||
.filter(p => p.plVideos.length > 0)
|
||
.sort((a, b) => {
|
||
// Prioritize playlists in progress
|
||
const aProgress = a.completedCount / a.plVideos.length;
|
||
const bProgress = b.completedCount / b.plVideos.length;
|
||
if (aProgress > 0 && aProgress < 1 && bProgress > 0 && bProgress < 1) return bProgress - aProgress;
|
||
if (aProgress > 0 && aProgress < 1) return -1;
|
||
if (bProgress > 0 && bProgress < 1) return 1;
|
||
return 0;
|
||
});
|
||
|
||
// Get recent videos from followed channels
|
||
const recentVideos = standaloneVideos
|
||
.filter(v => followedChannelIds.includes(v.channelId))
|
||
.concat(
|
||
followedPlaylists.flatMap(pl =>
|
||
(videos[pl.id] || []).slice(-2)
|
||
)
|
||
)
|
||
.sort(() => Math.random() - 0.5)
|
||
.slice(0, 8);
|
||
|
||
const totalVideosAvailable = followedPlaylists.reduce((sum, pl) => sum + (videos[pl.id]?.length || 0), 0) + standaloneVideos.filter(v => followedChannelIds.includes(v.channelId)).length;
|
||
const overallProgress = totalVideosAvailable > 0 ? Math.round((progress.watchedVideos.length / totalVideosAvailable) * 100) : 0;
|
||
|
||
return (
|
||
<div className="h-screen overflow-y-auto custom-scrollbar p-6 space-y-6 animate-slide-up">
|
||
{/* Hero Stats */}
|
||
<div className="bg-gradient-to-l from-orange-500 via-rose-500 to-purple-600 rounded-2xl p-6 text-white relative overflow-hidden">
|
||
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIyMCIgY3k9IjIwIiByPSIxIiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiLz48L3N2Zz4=')] opacity-50" />
|
||
<div className="relative">
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div>
|
||
<h2 className="text-2xl font-black mb-1">مرحبًا بك في مساري! 🎯</h2>
|
||
<p className="text-white/80 text-sm">واصل رحلة التعلم من حيث توقفت</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 bg-white/20 rounded-xl px-4 py-2">
|
||
<Flame className="w-5 h-5 text-yellow-300" />
|
||
<div className="text-right">
|
||
<div className="text-xl font-black">{progress.streak}</div>
|
||
<div className="text-[10px] text-white/70">أيام متتالية</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-4 gap-3">
|
||
<div className="bg-white/15 backdrop-blur-sm rounded-xl p-3 text-center">
|
||
<div className="text-2xl font-black">{progress.watchedVideos.length}</div>
|
||
<div className="text-[10px] text-white/70">فيديو مُشاهد</div>
|
||
</div>
|
||
<div className="bg-white/15 backdrop-blur-sm rounded-xl p-3 text-center">
|
||
<div className="text-2xl font-black">{progress.completedPlaylists.length}</div>
|
||
<div className="text-[10px] text-white/70">قائمة مكتملة</div>
|
||
</div>
|
||
<div className="bg-white/15 backdrop-blur-sm rounded-xl p-3 text-center">
|
||
<div className="text-2xl font-black">{followedChannels.length}</div>
|
||
<div className="text-[10px] text-white/70">قناة متابعة</div>
|
||
</div>
|
||
<div className="bg-white/15 backdrop-blur-sm rounded-xl p-3 text-center">
|
||
<div className="text-2xl font-black">{overallProgress}%</div>
|
||
<div className="text-[10px] text-white/70">نسبة الإنجاز</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Playlists Progress */}
|
||
<section>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||
<BookOpen className="w-5 h-5 text-purple-500" />
|
||
قوائم التشغيل الخاصة بك
|
||
</h3>
|
||
<span className="text-xs text-gray-400">{playlistsInProgress.length} قائمة</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{playlistsInProgress.slice(0, 9).map(({ playlist, channel, plVideos, completedCount }) => {
|
||
const percentage = plVideos.length > 0 ? Math.round((completedCount / plVideos.length) * 100) : 0;
|
||
const isComplete = progress.completedPlaylists.includes(playlist.id);
|
||
const nextVideoIndex = completedCount;
|
||
const nextVideo = plVideos[nextVideoIndex];
|
||
|
||
return (
|
||
<button
|
||
key={playlist.id}
|
||
onClick={() => setView({ type: 'playlist', playlistId: playlist.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"
|
||
>
|
||
<div className={`h-2 bg-gradient-to-l ${playlist.gradient}`} />
|
||
<div className="p-4">
|
||
<div className="flex items-start gap-3 mb-3">
|
||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${playlist.gradient} flex items-center justify-center text-2xl shrink-0 shadow-lg`}>
|
||
{playlist.emoji}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<h4 className="font-bold text-gray-800 text-sm truncate group-hover:text-orange-600 transition-colors">
|
||
{playlist.title}
|
||
</h4>
|
||
<p className="text-[11px] text-gray-400 mt-0.5">{channel?.name}</p>
|
||
</div>
|
||
{isComplete && (
|
||
<div className="bg-emerald-100 rounded-lg px-2 py-1">
|
||
<CheckCircle className="w-4 h-4 text-emerald-500" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Progress */}
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-[11px]">
|
||
<span className="text-gray-400">{completedCount} من {plVideos.length} فيديو</span>
|
||
<span className={`font-bold ${percentage === 100 ? 'text-emerald-500' : percentage > 0 ? 'text-orange-500' : 'text-gray-400'}`}>
|
||
{percentage}%
|
||
</span>
|
||
</div>
|
||
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full rounded-full transition-all duration-1000 ${
|
||
percentage === 100 ? 'bg-gradient-to-l from-emerald-400 to-green-500' : 'bg-gradient-to-l from-orange-400 to-rose-500'
|
||
}`}
|
||
style={{ width: `${percentage}%` }}
|
||
/>
|
||
</div>
|
||
{nextVideo && !isComplete && (
|
||
<div className="flex items-center gap-2 bg-orange-50 rounded-lg p-2 mt-2">
|
||
<Play className="w-3.5 h-3.5 text-orange-500 shrink-0" />
|
||
<span className="text-[11px] text-orange-700 truncate">التالي: {nextVideo.title}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Channel Grid */}
|
||
<section>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||
<Star className="w-5 h-5 text-amber-500" />
|
||
قنواتك المتابعة
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||
{followedChannels.map(ch => {
|
||
const chPlaylists = playlists.filter(p => p.channelId === ch.id);
|
||
const chVideosWatched = [...standaloneVideos, ...Object.values(videos).flat()].filter(
|
||
v => v.channelId === ch.id && progress.watchedVideos.includes(v.id)
|
||
).length;
|
||
|
||
return (
|
||
<button
|
||
key={ch.id}
|
||
onClick={() => setView({ type: 'channel', channelId: ch.id })}
|
||
className="group bg-white rounded-2xl p-4 shadow-sm hover:shadow-xl transition-all duration-300 text-right border border-gray-100 hover:border-purple-200"
|
||
>
|
||
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${ch.gradient} flex items-center justify-center text-3xl mx-auto mb-3 shadow-lg group-hover:scale-110 transition-transform`}>
|
||
{ch.emoji}
|
||
</div>
|
||
<h4 className="font-bold text-gray-800 text-sm truncate mb-1">{ch.name}</h4>
|
||
<p className="text-[10px] text-gray-400 mb-2">{formatNumber(ch.subscriberCount)} مشترك</p>
|
||
<div className="flex items-center justify-center gap-2 text-[10px]">
|
||
<span className="bg-blue-50 text-blue-600 px-2 py-0.5 rounded-full">{chPlaylists.length} دورة</span>
|
||
<span className="bg-emerald-50 text-emerald-600 px-2 py-0.5 rounded-full">{chVideosWatched} مُشاهد</span>
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
|
||
<button
|
||
onClick={() => setView({ type: 'browser' })}
|
||
className="bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl p-4 shadow-sm hover:shadow-xl transition-all duration-300 text-center border-2 border-dashed border-purple-200 hover:border-purple-400 group"
|
||
>
|
||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-3xl mx-auto mb-3 shadow-lg group-hover:scale-110 transition-transform">
|
||
➕
|
||
</div>
|
||
<h4 className="font-bold text-purple-700 text-sm">استكشف قنوات جديدة</h4>
|
||
<p className="text-[10px] text-purple-400 mt-1">أضف قنوات لبدء التعلم</p>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Recent Videos */}
|
||
{recentVideos.length > 0 && (
|
||
<section>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||
<Clock className="w-5 h-5 text-blue-500" />
|
||
جديد من قنواتك
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||
{recentVideos.map(video => {
|
||
const channel = getChannel(video.channelId);
|
||
const isWatched = progress.watchedVideos.includes(video.id);
|
||
|
||
return (
|
||
<button
|
||
key={video.id}
|
||
onClick={() => {
|
||
markVideoWatched(video.id);
|
||
setView({ type: 'video', videoId: video.id, playlistId: video.playlistId || undefined });
|
||
}}
|
||
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-blue-200 video-card"
|
||
>
|
||
<div className="relative">
|
||
<div className={`h-32 bg-gradient-to-br ${channel?.gradient || 'from-gray-400 to-gray-500'} flex items-center justify-center`}>
|
||
<span className="text-5xl opacity-50 group-hover:opacity-80 transition-opacity">{channel?.emoji || '🎬'}</span>
|
||
<div className="video-overlay absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 transition-opacity">
|
||
<Play className="w-10 h-10 text-white" />
|
||
</div>
|
||
</div>
|
||
<span className="absolute bottom-2 left-2 bg-black/70 text-white text-[10px] px-1.5 py-0.5 rounded-md">
|
||
{video.duration}
|
||
</span>
|
||
{isWatched && (
|
||
<div className="absolute top-2 right-2">
|
||
<CheckCircle className="w-5 h-5 text-emerald-400 drop-shadow-lg" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="p-3">
|
||
<h4 className="text-xs font-bold text-gray-800 line-clamp-2 mb-1.5 group-hover:text-orange-600 transition-colors">{video.title}</h4>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className={`w-5 h-5 rounded-md bg-gradient-to-br ${channel?.gradient || 'from-gray-400 to-gray-500'} flex items-center justify-center text-[10px]`}>
|
||
{channel?.emoji}
|
||
</span>
|
||
<span className="text-[10px] text-gray-400">{channel?.name}</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Achievements */}
|
||
<section className="pb-6">
|
||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2 mb-4">
|
||
<Trophy className="w-5 h-5 text-amber-500" />
|
||
الإنجازات
|
||
</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||
{[
|
||
{ emoji: '🌟', title: 'أول فيديو', desc: 'شاهد أول فيديو', done: progress.watchedVideos.length >= 1 },
|
||
{ emoji: '📚', title: 'طالب مجتهد', desc: 'شاهد 10 فيديوهات', done: progress.watchedVideos.length >= 10 },
|
||
{ emoji: '🔥', title: 'متسلسل', desc: '3 أيام متتالية', done: progress.streak >= 3 },
|
||
{ emoji: '🏆', title: 'منتهٍ', desc: 'أكمل أول قائمة', done: progress.completedPlaylists.length >= 1 },
|
||
{ emoji: '💎', title: 'محترف', desc: 'أكمل 3 قوائم', done: progress.completedPlaylists.length >= 3 },
|
||
{ emoji: '🎯', title: 'هادف', desc: 'شاهد 50 فيديو', done: progress.watchedVideos.length >= 50 },
|
||
{ emoji: '🚀', title: 'صاروخي', desc: '7 أيام متتالية', done: progress.streak >= 7 },
|
||
{ emoji: '👑', title: 'ملك التعلم', desc: 'أكمل 10 قوائم', done: progress.completedPlaylists.length >= 10 },
|
||
].map((ach, i) => (
|
||
<div
|
||
key={i}
|
||
className={`rounded-2xl p-4 text-center transition-all ${
|
||
ach.done
|
||
? 'bg-gradient-to-br from-amber-50 to-orange-50 border-2 border-amber-200 shadow-lg'
|
||
: 'bg-gray-50 border-2 border-gray-100 opacity-60'
|
||
}`}
|
||
>
|
||
<div className="text-3xl mb-2">{ach.emoji}</div>
|
||
<div className="text-sm font-bold text-gray-800">{ach.title}</div>
|
||
<div className="text-[10px] text-gray-400 mt-0.5">{ach.desc}</div>
|
||
{ach.done && <div className="text-[10px] text-emerald-500 font-bold mt-1">✅ مُحقق</div>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|