From 7a869f0e674df3774d11e50bb62107cb8dc67820 Mon Sep 17 00:00:00 2001 From: Mohamed-Abdelhalim2 Date: Wed, 17 Jun 2026 00:21:29 +0000 Subject: [PATCH] =?UTF-8?q?=D9=85=D9=84=D9=81=20=D8=A2=D8=AE=D8=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mohamed-Abdelhalim2 --- src/ChannelBrowser.tsx | 178 +++++++++++++++++++++++ src/ChannelView.tsx | 193 +++++++++++++++++++++++++ src/Dashboard.tsx | 297 ++++++++++++++++++++++++++++++++++++++ src/FolderView.tsx | 213 +++++++++++++++++++++++++++ src/PlaylistView.tsx | 320 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1201 insertions(+) create mode 100644 src/ChannelBrowser.tsx create mode 100644 src/ChannelView.tsx create mode 100644 src/Dashboard.tsx create mode 100644 src/FolderView.tsx create mode 100644 src/PlaylistView.tsx diff --git a/src/ChannelBrowser.tsx b/src/ChannelBrowser.tsx new file mode 100644 index 0000000..4d4c925 --- /dev/null +++ b/src/ChannelBrowser.tsx @@ -0,0 +1,178 @@ +import { useState } from 'react'; +import { useApp } from '../store'; +import { channels, formatNumber, getCategoryColor, getChannelPlaylists } from '../data'; +import { Search, Plus, Check, Filter, ArrowRight, List } from 'lucide-react'; + +const allCategories = ['الكل', 'تعليم', 'تقنية', 'برمجة', 'تاريخ', 'إسلام', 'حروب', 'اختراعات', 'دين', 'شريعة', 'لغة عربية', 'ريادة', 'أعمال', 'تصميم', 'ثقافة', 'سياسة', 'ابتكار', 'مشاريع']; + +export default function ChannelBrowser() { + const { followedChannelIds, followChannel, unfollowChannel, setView } = useApp(); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('الكل'); + const [showFilters, setShowFilters] = useState(false); + + const filteredChannels = channels.filter(ch => { + const matchesSearch = searchQuery === '' || + ch.name.includes(searchQuery) || + ch.description.includes(searchQuery) || + ch.categories.some(c => c.includes(searchQuery)); + + const matchesCategory = selectedCategory === 'الكل' || ch.categories.includes(selectedCategory); + + return matchesSearch && matchesCategory; + }); + + return ( +
+ {/* Header */} +
+

🔍 استكشف القنوات

+

اكتشف قنوات تعليمية متميزة وابدأ متابعتها

+
+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="ابحث عن قناة أو موضوع..." + className="w-full bg-white rounded-xl pr-11 pl-4 py-3 text-sm border border-gray-200 focus:border-purple-400 focus:ring-2 focus:ring-purple-100 outline-none transition-all" + /> +
+ +
+ + {/* Category Filters */} + {showFilters && ( +
+ {allCategories.map(cat => ( + + ))} +
+ )} + + {/* Content Filter Notice */} +
+ 🛡️ +
+

فلترة المحتوى نشطة

+

يتم فلترة المحتوى غير المناسب تلقائيًا

+
+
+ + {/* Channels Grid */} +
+ {filteredChannels.map(ch => { + const isFollowed = followedChannelIds.includes(ch.id); + const chPlaylists = getChannelPlaylists(ch.id); + + return ( +
+ {/* Channel Banner */} +
+
+
+
+ {ch.emoji} +
+
+
+ +
+
+
+

{ch.name}

+

{formatNumber(ch.subscriberCount)} مشترك · {ch.videoCount} فيديو

+
+ +
+ +

{ch.description}

+ + {/* Categories */} +
+ {ch.categories.slice(0, 3).map(cat => ( + + {cat} + + ))} +
+ + {/* Playlists Preview */} +
+ {chPlaylists.slice(0, 2).map(pl => ( + + ))} +
+ + +
+
+ ); + })} +
+ + {filteredChannels.length === 0 && ( +
+
🔍
+

لم يتم العثور على نتائج

+

جرّب البحث بكلمات مختلفة

+
+ )} +
+ ); +} diff --git a/src/ChannelView.tsx b/src/ChannelView.tsx new file mode 100644 index 0000000..9f2700c --- /dev/null +++ b/src/ChannelView.tsx @@ -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
القناة غير موجودة
; + } + + 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 ( +
+ {/* Channel Header */} +
+
+
+ +
+ + +
+
+ {channel.emoji} +
+
+

{channel.name}

+

{channel.description}

+
+ {formatNumber(channel.subscriberCount)} مشترك + + {channelPlaylists.length} قائمة تشغيل +
+
+ +
+ + {/* Categories */} +
+ {channel.categories.map(cat => ( + + {cat} + + ))} +
+
+
+ + {/* Stats */} +
+
+
{watchedCount}/{totalVideos}
+
فيديو مُشاهد
+
+
+
+ {channelPlaylists.filter(pl => progress.completedPlaylists.includes(pl.id)).length}/{channelPlaylists.length} +
+
قوائم مكتملة
+
+ {totalVideos > 0 && ( +
+
التقدم الكلي
+
+
+
+
{Math.round((watchedCount / totalVideos) * 100)}%
+
+ )} +
+ + {/* Playlists */} +
+

+ + قوائم التشغيل ({channelPlaylists.length}) +

+ +
+ {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 ( + + ); + })} +
+ + {/* Standalone Videos */} + {channelStandalone.length > 0 && ( + <> +

+

+
+ {channelStandalone.map(video => { + const isWatched = progress.watchedVideos.includes(video.id); + return ( + + ); + })} +
+ + )} +
+
+ ); +} diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx new file mode 100644 index 0000000..a4513bc --- /dev/null +++ b/src/Dashboard.tsx @@ -0,0 +1,297 @@ +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 ( +
+ {/* Hero Stats */} +
+
+
+
+
+

مرحبًا بك في مساري! 🎯

+

واصل رحلة التعلم من حيث توقفت

+
+
+ +
+
{progress.streak}
+
أيام متتالية
+
+
+
+ +
+
+
{progress.watchedVideos.length}
+
فيديو مُشاهد
+
+
+
{progress.completedPlaylists.length}
+
قائمة مكتملة
+
+
+
{followedChannels.length}
+
قناة متابعة
+
+
+
{overallProgress}%
+
نسبة الإنجاز
+
+
+
+
+ + {/* Playlists Progress */} +
+
+

+ + قوائم التشغيل الخاصة بك +

+ {playlistsInProgress.length} قائمة +
+ +
+ {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 ( + + ); + })} +
+
+ + {/* Channel Grid */} +
+
+

+ + قنواتك المتابعة +

+
+ +
+ {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 ( + + ); + })} + + +
+
+ + {/* Recent Videos */} + {recentVideos.length > 0 && ( +
+
+

+ + جديد من قنواتك +

+
+ +
+ {recentVideos.map(video => { + const channel = getChannel(video.channelId); + const isWatched = progress.watchedVideos.includes(video.id); + + return ( + + ); + })} +
+
+ )} + + {/* Achievements */} +
+

+ + الإنجازات +

+
+ {[ + { 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) => ( +
+
{ach.emoji}
+
{ach.title}
+
{ach.desc}
+ {ach.done &&
✅ مُحقق
} +
+ ))} +
+
+
+ ); +} diff --git a/src/FolderView.tsx b/src/FolderView.tsx new file mode 100644 index 0000000..c75fd8f --- /dev/null +++ b/src/FolderView.tsx @@ -0,0 +1,213 @@ +import { useState } from 'react'; +import { useApp } from '../store'; +import { Channel } from '../types'; +import { channels, getChannel, getChannelPlaylists, getPlaylistVideos } from '../data'; +import { ArrowRight, CheckCircle, Plus, X, FolderOpen, List } from 'lucide-react'; + +export default function FolderView({ folderId }: { folderId: string }) { + const { folders, progress, setView, followedChannelIds, addChannelToFolder, removeChannelFromFolder } = useApp(); + const [showAddChannel, setShowAddChannel] = useState(false); + + const folder = folders.find(f => f.id === folderId); + if (!folder) { + return
المجلد غير موجود
; + } + + const folderChannels = folder.channelIds.map(id => getChannel(id)).filter((c): c is Channel => c !== undefined); + const folderPlaylists = folder.channelIds.flatMap(chId => getChannelPlaylists(chId)); + + // Available channels not in folder + const availableChannels = channels.filter(ch => + followedChannelIds.includes(ch.id) && !folder.channelIds.includes(ch.id) + ); + + const totalVideos = folderPlaylists.reduce((sum, pl) => sum + (getPlaylistVideos(pl.id)?.length || 0), 0); + const watchedVideos = folderPlaylists.reduce((sum, pl) => { + return sum + getPlaylistVideos(pl.id).filter(v => progress.watchedVideos.includes(v.id)).length; + }, 0); + const overallPercentage = totalVideos > 0 ? Math.round((watchedVideos / totalVideos) * 100) : 0; + + const folderColorMap: Record = { + blue: 'from-blue-500 to-cyan-400', + purple: 'from-purple-500 to-indigo-400', + amber: 'from-amber-500 to-orange-400', + rose: 'from-rose-500 to-pink-400', + orange: 'from-orange-500 to-yellow-400', + green: 'from-green-500 to-emerald-400', + red: 'from-red-500 to-rose-400', + teal: 'from-teal-500 to-cyan-400', + }; + + const gradient = folderColorMap[folder.color] || 'from-gray-500 to-gray-400'; + + return ( +
+ {/* Folder Header */} +
+
+
+ + +
+
+ {folder.emoji} +
+
+

{folder.name}

+

{folderChannels.length} قناة · {folderPlaylists.length} قائمة تشغيل

+
+
+ {watchedVideos} من {totalVideos} فيديو +
+
+
+
+
+ {overallPercentage}% +
+
+
+ +
+
+
+ + {/* Add Channel Panel */} + {showAddChannel && ( +
+

إضافة قناة للمجلد

+ {availableChannels.length > 0 ? ( +
+ {availableChannels.map(ch => ( + + ))} +
+ ) : ( +

جميع القنوات المتابعة مضافة بالفعل

+ )} +
+ )} + + {/* Channels in Folder */} +
+

+ + القنوات في هذا المجلد +

+ +
+ {folderChannels.map(ch => { + const chPlaylists = getChannelPlaylists(ch.id); + return ( +
+ + +
+ ); + })} + + {availableChannels.length > 0 && ( + + )} +
+ + {/* Playlists */} +

+ + جميع قوائم التشغيل +

+ +
+ {folderPlaylists.map(pl => { + const plVideos = getPlaylistVideos(pl.id); + const ch = getChannel(pl.channelId); + if (!ch) return null; + 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 ( + + ); + })} +
+
+
+ ); +} diff --git a/src/PlaylistView.tsx b/src/PlaylistView.tsx new file mode 100644 index 0000000..dbec96c --- /dev/null +++ b/src/PlaylistView.tsx @@ -0,0 +1,320 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useApp } from '../store'; +import { getPlaylist, getPlaylistVideos, getChannel } from '../data'; +import { ArrowRight, Play, Pause, SkipForward, SkipBack, CheckCircle, RotateCcw } from 'lucide-react'; + +export default function PlaylistView({ playlistId }: { playlistId: string }) { + const { setView, progress, markVideoWatched, updatePlaylistProgress, completePlaylist } = useApp(); + const playlist = getPlaylist(playlistId); + const [currentIndex, setCurrentIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [showCelebration, setShowCelebration] = useState(false); + const [progressPercent, setProgressPercent] = useState(0); + const [autoPlay, setAutoPlay] = useState(true); + const [, setElapsed] = useState(0); + + const plVideos = getPlaylistVideos(playlistId); + const channel = playlist ? getChannel(playlist.channelId) : null; + + // Set initial index based on progress + useEffect(() => { + if (plVideos.length > 0) { + const lastWatched = progress.playlistProgress[playlistId]; + if (lastWatched !== undefined) { + // Check if all videos before are watched + let startFrom = 0; + for (let i = 0; i < plVideos.length; i++) { + if (!progress.watchedVideos.includes(plVideos[i].id)) { + startFrom = i; + break; + } + startFrom = i + 1; + } + setCurrentIndex(Math.min(startFrom, plVideos.length - 1)); + } + } + }, [playlistId]); + + const currentVideo = plVideos[currentIndex]; + const completedCount = plVideos.filter(v => progress.watchedVideos.includes(v.id)).length; + const totalVideos = plVideos.length; + const percentage = totalVideos > 0 ? Math.round((completedCount / totalVideos) * 100) : 0; + + // Simulate video playback progress + useEffect(() => { + let interval: ReturnType; + if (isPlaying && currentVideo) { + interval = setInterval(() => { + setElapsed(prev => { + const newElapsed = prev + 1; + const newPercent = Math.min(100, (newElapsed / currentVideo.durationSeconds) * 100); + setProgressPercent(newPercent); + + if (newPercent >= 100) { + handleVideoComplete(); + return 0; + } + return newElapsed; + }); + }, 100); // Speed up for demo + } + return () => clearInterval(interval); + }, [isPlaying, currentIndex]); + + const handleVideoComplete = useCallback(() => { + if (!currentVideo) return; + + markVideoWatched(currentVideo.id); + updatePlaylistProgress(playlistId, currentIndex); + setIsPlaying(false); + setProgressPercent(100); + + // Check if playlist is complete + const newCompletedCount = plVideos.filter(v => + progress.watchedVideos.includes(v.id) || v.id === currentVideo.id + ).length; + + if (newCompletedCount >= totalVideos) { + completePlaylist(playlistId); + setShowCelebration(true); + } else if (autoPlay) { + // Auto-play next video + setTimeout(() => { + if (currentIndex < totalVideos - 1) { + setCurrentIndex(prev => prev + 1); + setElapsed(0); + setProgressPercent(0); + setIsPlaying(true); + } + }, 1500); + } + }, [currentVideo, currentIndex, plVideos, totalVideos, autoPlay]); + + const goToVideo = (index: number) => { + if (index >= 0 && index < totalVideos) { + setCurrentIndex(index); + setElapsed(0); + setProgressPercent(0); + setIsPlaying(false); + } + }; + + const nextVideo = () => { + if (currentIndex < totalVideos - 1) { + goToVideo(currentIndex + 1); + } + }; + + const prevVideo = () => { + if (currentIndex > 0) { + goToVideo(currentIndex - 1); + } + }; + + if (!playlist || !channel || !currentVideo) { + return
قائمة التشغيل غير موجودة
; + } + + return ( +
+ {/* Celebration Overlay */} + {showCelebration && ( +
setShowCelebration(false)}> +
+
🎉
+

تهانينا!

+

لقد أكملت قائمة التشغيل

+
+
{playlist.title}
+
{channel.name}
+
+
+ + +
+
+
+ )} + + {/* Top Bar */} +
+ +
+
+ الفيديو + {currentIndex + 1}/{totalVideos} +
+
+
+
+
+ {percentage}% +
+
+
+ + {/* Main Content */} +
+ {/* Video Player Area */} +
+ {/* Video Display */} +
+
+ + {isPlaying ? ( +
+
{playlist.emoji}
+

{currentVideo.title}

+

جاري التشغيل...

+
+ ) : ( + + )} + + {/* Video Progress Bar */} +
+
+
+
+ + {/* Controls */} +
+
+ + + +
+ +
+ {currentVideo.duration} +
+ +
+ + +
+
+ + {/* Video Info */} +
+

{currentVideo.title}

+
+ + {currentVideo.viewCount.toLocaleString()} مشاهدة + {currentVideo.publishedAt} +
+
+
+ + {/* Playlist Sidebar */} +
+
+

{playlist.title}

+

{completedCount} من {totalVideos} مكتمل

+
+
+
+
+ +
+ {plVideos.map((video, index) => { + const isWatched = progress.watchedVideos.includes(video.id); + const isCurrent = index === currentIndex; + + return ( + + ); + })} +
+
+
+
+ ); +}