9 Commits
Author SHA1 Message Date
Mohamed-Abdelhalim2 ae2de982fd تحديث src/components/ChannelBrowser.tsx
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:23:16 +00:00
Mohamed-Abdelhalim2 3fe491ad1c تحديث src/components/ChannelView.tsx
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:23:04 +00:00
Mohamed-Abdelhalim2 faf8eb0619 تحديث src/components/Dashboard.tsx
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:22:44 +00:00
Mohamed-Abdelhalim2 9e9ac65c2e تحديث src/components/PlaylistView.tsx
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:22:04 +00:00
Mohamed-Abdelhalim2 7a869f0e67 ملف آخر
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:21:29 +00:00
Mohamed-Abdelhalim2 b45d2c5c0a ملف آخر
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:20:55 +00:00
Mohamed-Abdelhalim2 9e10e16a66 رفع الملفات إلى "src"
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:19:28 +00:00
Mohamed-Abdelhalim2 96b215daaf ملف آخر
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:18:54 +00:00
Mohamed-Abdelhalim2 848d9d7397 الملفات الأساسية
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
2026-06-17 00:18:10 +00:00
17 changed files with 5060 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>مساري - منصة التعلم المركّز</title>
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;800;900&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2573
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "react-vite-tailwind",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"clsx": "2.1.1",
"lucide-react": "^1.17.0",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "3.4.0"
},
"devDependencies": {
"@tailwindcss/vite": "4.1.17",
"@types/node": "22.19.17",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "5.1.1",
"tailwindcss": "4.1.17",
"typescript": "5.9.3",
"vite": "7.3.2",
"vite-plugin-singlefile": "2.3.0"
}
}
+102
View File
@@ -0,0 +1,102 @@
import { AppProvider, useApp } from './store';
import Sidebar from './components/Sidebar';
import Dashboard from './components/Dashboard';
import ChannelView from './components/ChannelView';
import PlaylistView from './components/PlaylistView';
import ChannelBrowser from './components/ChannelBrowser';
import FolderView from './components/FolderView';
import { Search, Bell, Settings } from 'lucide-react';
import { useState } from 'react';
function TopBar() {
const { currentView, searchQuery, setSearchQuery } = useApp();
const [focused, setFocused] = useState(false);
const getTitle = () => {
switch (currentView.type) {
case 'dashboard': return 'الرئيسية';
case 'browser': return 'استكشف القنوات';
case 'channel': return 'القناة';
case 'playlist': return 'قائمة التشغيل';
case 'folder': return 'المجلد';
default: return 'مساري';
}
};
if (currentView.type === 'playlist') return null;
return (
<div className="h-14 bg-white border-b border-gray-100 flex items-center justify-between px-5 shrink-0">
<div className="flex items-center gap-3">
<h2 className="text-lg font-bold text-gray-800">{getTitle()}</h2>
</div>
<div className="flex-1 max-w-md mx-8">
<div className={`relative transition-all ${focused ? 'scale-105' : ''}`}>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="ابحث في قنواتك..."
className={`w-full bg-gray-50 rounded-xl pr-10 pl-4 py-2 text-sm border transition-all outline-none ${
focused ? 'border-orange-300 ring-2 ring-orange-100 bg-white' : 'border-transparent'
}`}
/>
</div>
</div>
<div className="flex items-center gap-2">
<button className="relative p-2.5 rounded-xl hover:bg-gray-100 transition-colors text-gray-400 hover:text-gray-600">
<Bell className="w-4.5 h-4.5" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-orange-500 rounded-full" />
</button>
<button className="p-2.5 rounded-xl hover:bg-gray-100 transition-colors text-gray-400 hover:text-gray-600">
<Settings className="w-4.5 h-4.5" />
</button>
</div>
</div>
);
}
function MainContent() {
const { currentView } = useApp();
switch (currentView.type) {
case 'dashboard':
return <Dashboard />;
case 'channel':
return <ChannelView channelId={currentView.channelId} />;
case 'playlist':
return <PlaylistView playlistId={currentView.playlistId} />;
case 'browser':
return <ChannelBrowser />;
case 'folder':
return <FolderView folderId={currentView.folderId} />;
default:
return <Dashboard />;
}
}
function AppContent() {
return (
<div className="flex h-screen w-screen overflow-hidden bg-[#FFF8F0]" dir="rtl">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<TopBar />
<div className="flex-1 overflow-hidden">
<MainContent />
</div>
</div>
</div>
);
}
export default function App() {
return (
<AppProvider>
<AppContent />
</AppProvider>
);
}
+213
View File
@@ -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 <div className="p-8 text-center text-gray-400">المجلد غير موجود</div>;
}
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<string, string> = {
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 (
<div className="h-screen overflow-y-auto custom-scrollbar animate-slide-up">
{/* Folder Header */}
<div className={`bg-gradient-to-l ${gradient} p-8 relative overflow-hidden`}>
<div className="absolute inset-0 bg-black/15" />
<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">
{folder.emoji}
</div>
<div className="flex-1">
<h1 className="text-2xl font-black text-white mb-1">{folder.name}</h1>
<p className="text-white/70 text-sm mb-3">{folderChannels.length} قناة · {folderPlaylists.length} قائمة تشغيل</p>
<div className="flex items-center gap-4">
<div className="bg-white/20 backdrop-blur-sm rounded-lg px-3 py-1.5 text-white text-xs">
<span className="font-bold">{watchedVideos}</span> من <span className="font-bold">{totalVideos}</span> فيديو
</div>
<div className="flex items-center gap-2 bg-white/20 backdrop-blur-sm rounded-lg px-3 py-1.5">
<div className="h-2 w-24 bg-white/20 rounded-full overflow-hidden">
<div className="h-full bg-white/80 rounded-full" style={{ width: `${overallPercentage}%` }} />
</div>
<span className="text-white text-xs font-bold">{overallPercentage}%</span>
</div>
</div>
</div>
<button
onClick={() => setShowAddChannel(!showAddChannel)}
className="bg-white/20 backdrop-blur-sm text-white px-4 py-2 rounded-xl font-bold text-sm hover:bg-white/30 transition-all flex items-center gap-1.5"
>
<Plus className="w-4 h-4" />
إضافة قناة
</button>
</div>
</div>
</div>
{/* Add Channel Panel */}
{showAddChannel && (
<div className="bg-purple-50 border-b border-purple-200 p-4 animate-slide-up">
<h3 className="text-sm font-bold text-purple-800 mb-3">إضافة قناة للمجلد</h3>
{availableChannels.length > 0 ? (
<div className="flex gap-2 flex-wrap">
{availableChannels.map(ch => (
<button
key={ch.id}
onClick={() => addChannelToFolder(folderId, ch.id)}
className="flex items-center gap-2 bg-white rounded-xl px-3 py-2 text-sm border border-purple-200 hover:border-purple-400 transition-all"
>
<span>{ch.emoji}</span>
<span className="text-gray-700 font-bold text-xs">{ch.name}</span>
<Plus className="w-3 h-3 text-purple-500" />
</button>
))}
</div>
) : (
<p className="text-xs text-purple-400">جميع القنوات المتابعة مضافة بالفعل</p>
)}
</div>
)}
{/* Channels in Folder */}
<div className="p-6">
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2 mb-4">
<FolderOpen className="w-5 h-5 text-purple-500" />
القنوات في هذا المجلد
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 mb-8">
{folderChannels.map(ch => {
const chPlaylists = getChannelPlaylists(ch.id);
return (
<div key={ch.id} className="group relative">
<button
onClick={() => setView({ type: 'channel', channelId: ch.id })}
className="w-full bg-white rounded-2xl p-4 shadow-sm hover:shadow-xl transition-all duration-300 text-center border border-gray-100 hover:border-purple-200"
>
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${ch.gradient} flex items-center justify-center text-2xl mx-auto mb-2 shadow-lg group-hover:scale-110 transition-transform`}>
{ch.emoji}
</div>
<h4 className="font-bold text-gray-800 text-sm truncate">{ch.name}</h4>
<p className="text-[10px] text-gray-400 mt-0.5">{chPlaylists.length} قائمة</p>
</button>
<button
onClick={() => removeChannelFromFolder(folderId, ch.id)}
className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 bg-red-100 text-red-500 p-1 rounded-lg hover:bg-red-200 transition-all"
title="إزالة من المجلد"
>
<X className="w-3 h-3" />
</button>
</div>
);
})}
{availableChannels.length > 0 && (
<button
onClick={() => setShowAddChannel(true)}
className="bg-gray-50 rounded-2xl p-4 shadow-sm hover:shadow-xl transition-all duration-300 text-center border-2 border-dashed border-gray-200 hover:border-purple-300"
>
<div className="w-12 h-12 rounded-xl bg-gray-200 flex items-center justify-center text-2xl mx-auto mb-2">
<Plus className="w-6 h-6 text-gray-400" />
</div>
<h4 className="font-bold text-gray-400 text-sm">إضافة قناة</h4>
</button>
)}
</div>
{/* Playlists */}
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2 mb-4">
<List 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">
{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 (
<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-orange-200"
>
<div className={`h-24 bg-gradient-to-br ${pl.gradient} flex items-center justify-center relative`}>
<span className="text-4xl opacity-40 group-hover:opacity-70 transition-opacity">{pl.emoji}</span>
{isComplete && (
<div className="absolute top-2 right-2 bg-emerald-500 text-white text-[10px] px-2 py-0.5 rounded-lg flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
مكتملة
</div>
)}
</div>
<div className="p-4">
<h4 className="font-bold text-gray-800 text-sm mb-1 group-hover:text-orange-600 transition-colors truncate">{pl.title}</h4>
<div className="flex items-center gap-1.5 mb-2">
<span className={`w-4 h-4 rounded bg-gradient-to-br ${ch.gradient} flex items-center justify-center text-[8px]`}>
{ch.emoji}
</span>
<span className="text-[10px] text-gray-400">{ch.name}</span>
</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden mb-1.5">
<div
className={`h-full rounded-full ${percentage === 100 ? 'bg-emerald-400' : 'bg-gradient-to-l from-orange-400 to-rose-500'}`}
style={{ width: `${percentage}%` }}
/>
</div>
<div className="flex justify-between text-[10px]">
<span className="text-gray-400">{completedCount}/{plVideos.length}</span>
<span className={`font-bold ${percentage === 100 ? 'text-emerald-500' : 'text-orange-500'}`}>{percentage}%</span>
</div>
</div>
</button>
);
})}
</div>
</div>
</div>
);
}
+178
View File
@@ -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 (
<div className="h-screen overflow-y-auto custom-scrollbar p-6 animate-slide-up">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-black text-gray-800 mb-2">🔍 استكشف القنوات</h1>
<p className="text-gray-400 text-sm">اكتشف قنوات تعليمية متميزة وابدأ متابعتها</p>
</div>
{/* Search */}
<div className="flex gap-3 mb-4">
<div className="flex-1 relative">
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-gray-400" />
<input
value={searchQuery}
onChange={e => 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"
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={`px-4 py-3 rounded-xl font-bold text-sm flex items-center gap-2 transition-all ${
showFilters ? 'bg-purple-500 text-white' : 'bg-white text-gray-600 border border-gray-200 hover:border-purple-300'
}`}
>
<Filter className="w-4 h-4" />
تصفية
</button>
</div>
{/* Category Filters */}
{showFilters && (
<div className="flex gap-2 flex-wrap mb-6 animate-slide-up">
{allCategories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all ${
selectedCategory === cat
? 'bg-purple-500 text-white shadow-lg shadow-purple-500/25'
: 'bg-white text-gray-500 border border-gray-200 hover:border-purple-300 hover:text-purple-600'
}`}
>
{cat}
</button>
))}
</div>
)}
{/* Content Filter Notice */}
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-3 mb-6 flex items-center gap-2">
<span className="text-lg">🛡️</span>
<div>
<p className="text-xs font-bold text-emerald-700">فلترة المحتوى نشطة</p>
<p className="text-[10px] text-emerald-500">يتم فلترة المحتوى غير المناسب تلقائيًا</p>
</div>
</div>
{/* Channels Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredChannels.map(ch => {
const isFollowed = followedChannelIds.includes(ch.id);
const chPlaylists = getChannelPlaylists(ch.id);
return (
<div
key={ch.id}
className="bg-white rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-purple-200"
>
{/* Channel Banner */}
<div className={`h-24 bg-gradient-to-l ${ch.gradient} relative`}>
<div className="absolute inset-0 bg-black/10" />
<div className="absolute -bottom-6 right-4">
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${ch.gradient} flex items-center justify-center text-3xl shadow-lg border-4 border-white`}>
{ch.emoji}
</div>
</div>
</div>
<div className="p-4 pt-8">
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-800 text-base truncate">{ch.name}</h3>
<p className="text-[11px] text-gray-400">{formatNumber(ch.subscriberCount)} مشترك · {ch.videoCount} فيديو</p>
</div>
<button
onClick={() => isFollowed ? unfollowChannel(ch.id) : followChannel(ch.id)}
className={`px-4 py-1.5 rounded-xl font-bold text-xs flex items-center gap-1 transition-all shrink-0 ${
isFollowed
? 'bg-emerald-50 text-emerald-600 hover:bg-red-50 hover:text-red-500'
: 'bg-gradient-to-l from-purple-500 to-indigo-500 text-white hover:from-purple-600 hover:to-indigo-600 shadow-lg shadow-purple-500/25'
}`}
>
{isFollowed ? <><Check className="w-3.5 h-3.5" /> متابَع</> : <><Plus className="w-3.5 h-3.5" /> متابعة</>}
</button>
</div>
<p className="text-[11px] text-gray-400 mb-3 line-clamp-2">{ch.description}</p>
{/* Categories */}
<div className="flex gap-1.5 flex-wrap mb-3">
{ch.categories.slice(0, 3).map(cat => (
<span key={cat} className={`text-[9px] px-2 py-0.5 rounded-full ${getCategoryColor(cat)}`}>
{cat}
</span>
))}
</div>
{/* Playlists Preview */}
<div className="space-y-1.5">
{chPlaylists.slice(0, 2).map(pl => (
<button
key={pl.id}
onClick={() => {
if (isFollowed) {
setView({ type: 'playlist', playlistId: pl.id });
} else {
followChannel(ch.id);
setView({ type: 'playlist', playlistId: pl.id });
}
}}
className="w-full flex items-center gap-2 bg-gray-50 hover:bg-orange-50 rounded-lg px-3 py-2 text-right transition-colors group"
>
<span className="text-sm">{pl.emoji}</span>
<span className="text-[11px] text-gray-600 group-hover:text-orange-700 flex-1 truncate">{pl.title}</span>
<List className="w-3 h-3 text-gray-300" />
<span className="text-[10px] text-gray-400">{pl.videoCount}</span>
</button>
))}
</div>
<button
onClick={() => setView({ type: 'channel', channelId: ch.id })}
className="w-full mt-3 flex items-center justify-center gap-1 text-purple-500 hover:text-purple-700 text-xs font-bold transition-colors"
>
عرض القناة
<ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
})}
</div>
{filteredChannels.length === 0 && (
<div className="text-center py-12">
<div className="text-5xl mb-4">🔍</div>
<h3 className="text-lg font-bold text-gray-400">لم يتم العثور على نتائج</h3>
<p className="text-sm text-gray-300 mt-1">جرّب البحث بكلمات مختلفة</p>
</div>
)}
</div>
);
}
+193
View 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>
);
}
+297
View File
@@ -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 (
<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>
);
}
+320
View File
@@ -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<typeof setInterval>;
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 <div className="p-8 text-center text-gray-400">قائمة التشغيل غير موجودة</div>;
}
return (
<div className="h-screen flex flex-col overflow-hidden">
{/* Celebration Overlay */}
{showCelebration && (
<div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center animate-slide-up" onClick={() => setShowCelebration(false)}>
<div className="bg-white rounded-3xl p-8 text-center max-w-md mx-4 shadow-2xl">
<div className="text-6xl mb-4 animate-float">🎉</div>
<h2 className="text-2xl font-black text-gray-800 mb-2">تهانينا!</h2>
<p className="text-gray-500 mb-4">لقد أكملت قائمة التشغيل</p>
<div className="bg-gradient-to-l from-orange-500 to-rose-500 rounded-xl p-4 text-white mb-4">
<div className="text-lg font-bold">{playlist.title}</div>
<div className="text-sm text-white/80">{channel.name}</div>
</div>
<div className="flex gap-3">
<button onClick={() => { setShowCelebration(false); setView({ type: 'dashboard' }); }} className="flex-1 bg-orange-500 text-white font-bold py-3 rounded-xl hover:bg-orange-600 transition-colors">
العودة للرئيسية
</button>
<button onClick={() => setShowCelebration(false)} className="flex-1 bg-gray-100 text-gray-600 font-bold py-3 rounded-xl hover:bg-gray-200 transition-colors">
إغلاق
</button>
</div>
</div>
</div>
)}
{/* Top Bar */}
<div className="flex items-center justify-between px-4 py-3 bg-white border-b border-gray-100 shrink-0">
<button
onClick={() => setView({ type: 'channel', channelId: channel.id })}
className="flex items-center gap-1 text-gray-400 hover:text-gray-600 text-sm transition-colors"
>
<ArrowRight className="w-4 h-4" />
{channel.name}
</button>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 bg-gray-50 rounded-lg px-3 py-1.5">
<span className="text-xs text-gray-400">الفيديو</span>
<span className="text-sm font-bold text-gray-700">{currentIndex + 1}/{totalVideos}</span>
</div>
<div className="flex items-center gap-2">
<div className="h-2 w-32 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${percentage === 100 ? 'bg-emerald-400' : 'bg-gradient-to-l from-orange-400 to-rose-500'}`}
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-xs font-bold text-gray-500">{percentage}%</span>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex overflow-hidden">
{/* Video Player Area */}
<div className="flex-1 flex flex-col">
{/* Video Display */}
<div className="flex-1 bg-black relative flex items-center justify-center">
<div className={`absolute inset-0 bg-gradient-to-br ${playlist.gradient} opacity-30`} />
{isPlaying ? (
<div className="relative text-center">
<div className="text-8xl mb-4 animate-pulse">{playlist.emoji}</div>
<h3 className="text-white text-xl font-bold max-w-lg mx-auto">{currentVideo.title}</h3>
<p className="text-white/50 text-sm mt-2">جاري التشغيل...</p>
</div>
) : (
<button
onClick={() => setIsPlaying(true)}
className="relative text-center group cursor-pointer"
>
<div className="w-24 h-24 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center mx-auto mb-4 group-hover:bg-white/30 transition-all group-hover:scale-110">
<Play className="w-10 h-10 text-white mr-[-3px]" />
</div>
<h3 className="text-white text-xl font-bold max-w-lg mx-auto">{currentVideo.title}</h3>
<p className="text-white/50 text-sm mt-2">اضغط للتشغيل</p>
</button>
)}
{/* Video Progress Bar */}
<div className="absolute bottom-0 left-0 right-0 h-1.5 bg-white/20">
<div
className="h-full bg-gradient-to-l from-orange-400 to-rose-400 transition-all"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
{/* Controls */}
<div className="bg-[#1a1a2e] px-6 py-3 flex items-center justify-between shrink-0">
<div className="flex items-center gap-4">
<button onClick={prevVideo} disabled={currentIndex === 0} className="text-white/60 hover:text-white disabled:opacity-30 transition-colors">
<SkipForward className="w-5 h-5" />
</button>
<button
onClick={() => {
if (isPlaying) {
setIsPlaying(false);
} else {
setIsPlaying(true);
markVideoWatched(currentVideo.id);
}
}}
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${
isPlaying ? 'bg-white/10 hover:bg-white/20' : 'bg-orange-500 hover:bg-orange-600 shadow-lg shadow-orange-500/30'
}`}
>
{isPlaying ? <Pause className="w-5 h-5 text-white" /> : <Play className="w-5 h-5 text-white mr-[-2px]" />}
</button>
<button onClick={nextVideo} disabled={currentIndex === totalVideos - 1} className="text-white/60 hover:text-white disabled:opacity-30 transition-colors">
<SkipBack className="w-5 h-5" />
</button>
</div>
<div className="text-white/50 text-xs">
{currentVideo.duration}
</div>
<div className="flex items-center gap-3">
<button onClick={() => { setElapsed(0); setProgressPercent(0); }} className="text-white/40 hover:text-white/70 transition-colors">
<RotateCcw className="w-4 h-4" />
</button>
<label className="flex items-center gap-1.5 text-white/40 text-[10px] cursor-pointer">
<input
type="checkbox"
checked={autoPlay}
onChange={e => setAutoPlay(e.target.checked)}
className="w-3 h-3 accent-orange-500"
/>
تشغيل تلقائي
</label>
</div>
</div>
{/* Video Info */}
<div className="bg-white px-6 py-4 border-b border-gray-100 shrink-0">
<h2 className="text-lg font-bold text-gray-800">{currentVideo.title}</h2>
<div className="flex items-center gap-3 mt-2">
<button
onClick={() => setView({ type: 'channel', channelId: channel.id })}
className={`flex items-center gap-2 bg-gradient-to-l ${channel.gradient} text-white px-3 py-1.5 rounded-lg text-xs font-bold hover:opacity-90 transition-opacity`}
>
{channel.emoji} {channel.name}
</button>
<span className="text-xs text-gray-400">{currentVideo.viewCount.toLocaleString()} مشاهدة</span>
<span className="text-xs text-gray-400">{currentVideo.publishedAt}</span>
</div>
</div>
</div>
{/* Playlist Sidebar */}
<div className="w-80 bg-gray-50 border-r border-gray-200 flex flex-col shrink-0">
<div className="p-4 border-b border-gray-200">
<h3 className="font-bold text-gray-800 text-sm">{playlist.title}</h3>
<p className="text-[11px] text-gray-400 mt-1">{completedCount} من {totalVideos} مكتمل</p>
<div className="h-1.5 bg-gray-200 rounded-full mt-2 overflow-hidden">
<div
className={`h-full rounded-full ${percentage === 100 ? 'bg-emerald-400' : 'bg-gradient-to-l from-orange-400 to-rose-500'}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{plVideos.map((video, index) => {
const isWatched = progress.watchedVideos.includes(video.id);
const isCurrent = index === currentIndex;
return (
<button
key={video.id}
onClick={() => goToVideo(index)}
className={`w-full flex items-center gap-3 px-4 py-3 text-right transition-all border-b border-gray-100 ${
isCurrent
? 'bg-orange-50 border-r-4 border-r-orange-500'
: isWatched
? 'bg-emerald-50/50'
: 'hover:bg-gray-100'
}`}
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 text-xs font-bold ${
isCurrent
? 'bg-orange-500 text-white shadow-lg shadow-orange-500/30'
: isWatched
? 'bg-emerald-100 text-emerald-600'
: 'bg-gray-200 text-gray-500'
}`}>
{isCurrent ? <Play className="w-3.5 h-3.5" /> : isWatched ? <CheckCircle className="w-3.5 h-3.5" /> : index + 1}
</div>
<div className="flex-1 min-w-0">
<h4 className={`text-xs font-bold truncate ${isCurrent ? 'text-orange-700' : isWatched ? 'text-emerald-700' : 'text-gray-700'}`}>
{video.title}
</h4>
<span className="text-[10px] text-gray-400">{video.duration}</span>
</div>
</button>
);
})}
</div>
</div>
</div>
</div>
);
}
+679
View File
@@ -0,0 +1,679 @@
import { Channel, Playlist, Video, Folder } from './types';
// ── Channels ──────────────────────────────────────────────
export const channels: Channel[] = [
{
id: 'ch-tech',
name: 'أبدع تقنيًا',
description: 'قناة تعليمية متخصصة في البرمجة والتقنية وتطوير البرمجيات. نقدم دورات تعليمية شاملة في مختلف لغات البرمجة والتقنيات الحديثة.',
gradient: 'from-blue-500 to-cyan-400',
emoji: '💻',
categories: ['تعليم', 'تقنية', 'برمجة', 'ذكاء اصطناعي'],
subscriberCount: 1250000,
videoCount: 342,
},
{
id: 'ch-history',
name: 'تاريخنا المجيد',
description: 'قناة مخصصة لتاريخ الإسلام والأمة العربية. نستعرض أحداث التاريخ الإسلامي والعربي بأسلوب شيق ومبسط.',
gradient: 'from-amber-500 to-orange-400',
emoji: '🏰',
categories: ['تاريخ', 'إسلام', 'حروب', 'حضارة', 'تراث'],
subscriberCount: 2100000,
videoCount: 256,
},
{
id: 'ch-invent',
name: 'المخترع العربي',
description: 'قناة تهتم بالاختراعات والابتكارات العربية والعالمية. نصنع ونشرح ونبني مشاريع تقنية وعلمية متنوعة.',
gradient: 'from-emerald-500 to-teal-400',
emoji: '🔧',
categories: ['اختراعات', 'تقنية', 'ابتكار', 'روبوتات', 'إلكترونيات'],
subscriberCount: 890000,
videoCount: 178,
},
{
id: 'ch-sharia',
name: 'علوم الشريعة',
description: 'قناة علمية شرعية متخصصة في العلوم الشرعية والفقه والعقيدة والتفسير والحديث. نهج أهل السنة والجماعة.',
gradient: 'from-purple-500 to-indigo-400',
emoji: '📖',
categories: ['دين', 'شريعة', 'فقه', 'عقيدة', 'تفسير', 'حديث'],
subscriberCount: 3200000,
videoCount: 520,
},
{
id: 'ch-lang',
name: 'لغتنا الجميلة',
description: 'قناة مخصصة للغة العربية الفصحى والنحو والصرف والبلاغة والأدب العربي والشعر والنثر.',
gradient: 'from-rose-500 to-pink-400',
emoji: '✨',
categories: ['لغة عربية', 'أدب', 'شعر', 'نثر', 'بلاغة', 'نحو'],
subscriberCount: 760000,
videoCount: 195,
},
{
id: 'ch-biz',
name: 'ريادة المستقبل',
description: 'قناة متخصصة في ريادة الأعمال والمشاريع الناشئة وقصص النجاح العربية. نقدم نصائح عملية لرواد الأعمال.',
gradient: 'from-orange-500 to-yellow-400',
emoji: '🚀',
categories: ['ريادة', 'أعمال', 'إدارة', 'تسويق', 'اقتصاد'],
subscriberCount: 540000,
videoCount: 134,
},
{
id: 'ch-design',
name: 'عالم التصميم',
description: 'قناة متخصصة في التصميم الهندسي والمعماري. نستعرض أعظم التصاميم في العالم والمشاريع المعمارية الفريدة.',
gradient: 'from-violet-500 to-fuchsia-400',
emoji: '🎨',
categories: ['تصميم', 'هندسة', 'عمارة', 'فن إسلامي'],
subscriberCount: 430000,
videoCount: 98,
},
{
id: 'ch-projects',
name: 'مشاريع عربية',
description: 'قناة تعرض المشاريع العربية والإسلامية الكبرى في مختلف المجالات التقنية والإنشائية والاقتصادية.',
gradient: 'from-sky-500 to-blue-400',
emoji: '🏗️',
categories: ['مشاريع', 'تقنية', 'ابتكار', 'اقتصاد'],
subscriberCount: 380000,
videoCount: 87,
},
{
id: 'ch-culture',
name: 'ثقافتنا هويتنا',
description: 'قناة تهتم بالثقافة العربية والإسلامية والعادات والتقاليد واللباس الإسلامي والتراث العربي الأصيل.',
gradient: 'from-red-500 to-rose-400',
emoji: '🕌',
categories: ['ثقافة', 'تراث', 'حضارة', 'إسلام'],
subscriberCount: 920000,
videoCount: 167,
},
{
id: 'ch-innovation',
name: 'عالم الابتكار',
description: 'قناة عن الابتكار والتصميم الحديث والاختراعات الثورية التي تغير العالم. نسلط الضوء على القمة في التصميم.',
gradient: 'from-teal-500 to-emerald-400',
emoji: '💡',
categories: ['ابتكار', 'تصميم', 'اختراعات', 'تقنية'],
subscriberCount: 670000,
videoCount: 143,
},
{
id: 'ch-politics',
name: 'ساحة السياسة',
description: 'قناة تحليلية سياسية تتناول القضايا السياسية العربية والإسلامية والصراعات في المنطقة بأسلوب موضوعي.',
gradient: 'from-slate-600 to-gray-500',
emoji: '⚖️',
categories: ['سياسة', 'تاريخ', 'حروب', 'قانون'],
subscriberCount: 1800000,
videoCount: 312,
},
{
id: 'ch-wars',
name: 'الملاحم والفتوحات',
description: 'قناة مخصصة لتاريخ الحروب العربية والإسلامية والفتوحات والمعارك الكبرى والقادة العسكريين المسلمين.',
gradient: 'from-zinc-700 to-stone-600',
emoji: '⚔️',
categories: ['حروب', 'تاريخ', 'عسكرية', 'استراتيجية', 'إسلام'],
subscriberCount: 1500000,
videoCount: 245,
},
];
// ── Playlists ─────────────────────────────────────────────
function makeVideos(channelId: string, playlistId: string, titles: string[], durations: number[]): Video[] {
return titles.map((title, i) => ({
id: `v-${channelId}-${playlistId}-${i}`,
playlistId,
channelId,
title,
description: `${title} - شرح مفصل مع أمثلة وتطبيقات عملية.`,
duration: `${Math.floor(durations[i] / 60)}:${String(durations[i] % 60).padStart(2, '0')}`,
durationSeconds: durations[i],
viewCount: Math.floor(Math.random() * 500000) + 10000,
publishedAt: `2024-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
position: i,
}));
}
export const playlists: Playlist[] = [
// Tech Channel
{
id: 'pl-tech-1',
channelId: 'ch-tech',
title: 'أساسيات البرمجة بلغة Python',
description: 'دورة شاملة لتعلم أساسيات البرمجة بلغة بايثون من الصفر حتى الاحتراف',
videoCount: 12,
gradient: 'from-blue-600 to-cyan-500',
emoji: '🐍',
categories: ['برمجة', 'تقنية', 'تعليم'],
},
{
id: 'pl-tech-2',
channelId: 'ch-tech',
title: 'تطوير تطبيقات الويب الكاملة',
description: 'دورة تطوير تطبيقات الويب من الواجهة إلى الخادم باستخدام React و Node.js',
videoCount: 8,
gradient: 'from-indigo-500 to-blue-400',
emoji: '🌐',
categories: ['برمجة', 'تقنية', 'تعليم'],
},
{
id: 'pl-tech-3',
channelId: 'ch-tech',
title: 'الذكاء الاصطناعي للمبتدئين',
description: 'مقدمة شاملة في الذكاء الاصطناعي وتعلم الآلة والشبكات العصبية',
videoCount: 10,
gradient: 'from-purple-500 to-pink-400',
emoji: '🤖',
categories: ['ذكاء اصطناعي', 'تقنية', 'تعليم'],
},
// History Channel
{
id: 'pl-hist-1',
channelId: 'ch-history',
title: 'فتوحات الإسلام الكبرى',
description: 'سلسلة عن الفتوحات الإسلامية الكبرى من عهد النبوة إلى الدولة الأموية',
videoCount: 15,
gradient: 'from-amber-600 to-orange-500',
emoji: '⚔️',
categories: ['تاريخ', 'إسلام', 'حروب'],
},
{
id: 'pl-hist-2',
channelId: 'ch-history',
title: 'حضارة الأندلس الإسلامية',
description: 'رحلة في تاريخ الأندلس الإسلامية depuis الفتح حتى السقوط',
videoCount: 8,
gradient: 'from-yellow-500 to-amber-400',
emoji: '🏛️',
categories: ['تاريخ', 'حضارة', 'إسلام'],
},
{
id: 'pl-hist-3',
channelId: 'ch-history',
title: 'عظماء المسلمين',
description: 'سير وحكايات عظماء المسلمين الذين غيروا مجرى التاريخ',
videoCount: 12,
gradient: 'from-orange-600 to-red-400',
emoji: '🌟',
categories: ['تاريخ', 'إسلام', 'سيرة'],
},
// Inventor Channel
{
id: 'pl-inv-1',
channelId: 'ch-invent',
title: 'اختراعات غيرت العالم',
description: 'سلسلة عن أعظم الاختراعات التي غيرت مسار البشرية',
videoCount: 10,
gradient: 'from-emerald-600 to-green-400',
emoji: '🔧',
categories: ['اختراعات', 'ابتكار', 'علوم'],
},
{
id: 'pl-inv-2',
channelId: 'ch-invent',
title: 'اصنع روبوتك بنفسك',
description: 'دورة عملية لبناء روبوت من الصفر باستخدام Arduino',
videoCount: 8,
gradient: 'from-teal-500 to-emerald-400',
emoji: '🤖',
categories: ['روبوتات', 'إلكترونيات', 'اختراعات'],
},
// Sharia Channel
{
id: 'pl-sharia-1',
channelId: 'ch-sharia',
title: 'فقه العبادات الميسّر',
description: 'شرح ميسر لأحكام العبادات من الطهارة إلى الحج',
videoCount: 20,
gradient: 'from-purple-600 to-indigo-500',
emoji: '🕌',
categories: ['فقه', 'شريعة', 'دين'],
},
{
id: 'pl-sharia-2',
channelId: 'ch-sharia',
title: 'أصول العقيدة الإسلامية',
description: 'شرح أصول عقيدة أهل السنة والجماعة',
videoCount: 15,
gradient: 'from-indigo-600 to-violet-500',
emoji: '💫',
categories: ['عقيدة', 'شريعة', 'دين'],
},
// Language Channel
{
id: 'pl-lang-1',
channelId: 'ch-lang',
title: 'النحو الواضح المبسّط',
description: 'دورة في النحو العربي مبسطة وميسرة للمبتدئين والمتقدمين',
videoCount: 12,
gradient: 'from-rose-500 to-pink-400',
emoji: '📝',
categories: ['لغة عربية', 'نحو', 'تعليم'],
},
{
id: 'pl-lang-2',
channelId: 'ch-lang',
title: 'بلاغة القرآن الكريم',
description: 'دراسة بلاغة القرآن والإعجاز اللغوي في آياته الكريمة',
videoCount: 10,
gradient: 'from-pink-500 to-rose-400',
emoji: '📜',
categories: ['بلاغة', 'لغة عربية', 'قرآن'],
},
// Business Channel
{
id: 'pl-biz-1',
channelId: 'ch-biz',
title: 'كيف تبني شركة ناشئة ناجحة',
description: 'دليلك الشامل لبناء وإدارة شركة ناشئة من الفكرة إلى التنفيذ',
videoCount: 8,
gradient: 'from-orange-500 to-amber-400',
emoji: '🏢',
categories: ['ريادة', 'أعمال', 'إدارة'],
},
{
id: 'pl-biz-2',
channelId: 'ch-biz',
title: 'قصص نجاح عربية ملهمة',
description: 'قصص نجاح حقيقية لرواد أعمال ومبتكرين عرب غيروا الواقع',
videoCount: 6,
gradient: 'from-yellow-500 to-orange-400',
emoji: '⭐',
categories: ['ريادة', 'أعمال', 'قصص'],
},
// Design Channel
{
id: 'pl-design-1',
channelId: 'ch-design',
title: 'أساسيات التصميم الهندسي',
description: 'مدخل إلى عالم التصميم الهندسي والمعماري',
videoCount: 10,
gradient: 'from-violet-500 to-purple-400',
emoji: '✏️',
categories: ['تصميم', 'هندسة', 'عمارة'],
},
{
id: 'pl-design-2',
channelId: 'ch-design',
title: 'أعظم التصاميم المعمارية الإسلامية',
description: 'جولة في أعظم المباني والتصاميم المعمارية الإسلامية عبر التاريخ',
videoCount: 8,
gradient: 'from-fuchsia-500 to-violet-400',
emoji: '🕌',
categories: ['عمارة', 'فن إسلامي', 'تصميم'],
},
// Projects Channel
{
id: 'pl-proj-1',
channelId: 'ch-projects',
title: 'مشاريع تقنية عربية رائدة',
description: 'استعراض أبرز المشاريع التقنية العربية التي تنافس عالميًا',
videoCount: 6,
gradient: 'from-sky-500 to-blue-400',
emoji: '💻',
categories: ['مشاريع', 'تقنية', 'ابتكار'],
},
// Culture Channel
{
id: 'pl-cult-1',
channelId: 'ch-culture',
title: 'العادات والتقاليد العربية',
description: 'رحلة في العادات والتقاليد العربية الأصيلة عبر البلدان العربية',
videoCount: 8,
gradient: 'from-red-500 to-rose-400',
emoji: '🌍',
categories: ['ثقافة', 'تراث', 'حضارة'],
},
{
id: 'pl-cult-2',
channelId: 'ch-culture',
title: 'اللباس الإسلامي عبر التاريخ',
description: 'تطور اللباس الإسلامي والعربي عبر العصور ودلالاته الثقافية',
videoCount: 6,
gradient: 'from-rose-500 to-red-400',
emoji: '👔',
categories: ['ثقافة', 'تراث', 'إسلام'],
},
// Innovation Channel
{
id: 'pl-innov-1',
channelId: 'ch-innovation',
title: 'الابتكار في العالم العربي',
description: 'قصص الابتكار والإبداع في العالم العربي والمشاريع المتميزة',
videoCount: 8,
gradient: 'from-teal-500 to-cyan-400',
emoji: '💡',
categories: ['ابتكار', 'مشاريع', 'تقنية'],
},
// Politics Channel
{
id: 'pl-pol-1',
channelId: 'ch-politics',
title: 'القضية الفلسطينية: تاريخ ومستقبل',
description: 'سلسلة تحليلية شاملة عن القضية الفلسطينية depuis بدايتها حتى اليوم',
videoCount: 12,
gradient: 'from-slate-600 to-gray-500',
emoji: '🇵🇸',
categories: ['سياسة', 'تاريخ', 'قانون'],
},
// Wars Channel
{
id: 'pl-wars-1',
channelId: 'ch-wars',
title: 'حروب العرب عبر التاريخ',
description: 'سلسلة عن أبرز الحروب والمعارك التي خاضها العرب والمسلمون',
videoCount: 15,
gradient: 'from-zinc-700 to-stone-500',
emoji: '⚔️',
categories: ['حروب', 'تاريخ', 'عسكرية'],
},
{
id: 'pl-wars-2',
channelId: 'ch-wars',
title: 'القادة العسكريون المسلمون',
description: 'سير أعظم القادة العسكريين في التاريخ الإسلامي',
videoCount: 10,
gradient: 'from-stone-600 to-zinc-500',
emoji: '🗡️',
categories: ['حروب', 'تاريخ', 'سيرة', 'عسكرية'],
},
];
// ── Video Data ────────────────────────────────────────────
const videoTitlesMap: Record<string, string[]> = {
'pl-tech-1': [
'مقدمة في لغة Python وتثبيت البيئة', 'المتغيرات وأنواع البيانات',
'العمليات الحسابية والمنطقية', 'الشروط وجملة if-else',
'الحلقات التكرارية for و while', 'القوائم (Lists) والعمليات عليها',
'القواميس (Dictionaries) والتعامل معها', 'الدوال (Functions) والنطاق',
'البرمجة الكائنية (OOP) - الجزء الأول', 'البرمجة الكائنية (OOP) - الجزء الثاني',
'التعامل مع الملفات', 'مشروع تطبيقي: بناء برنامج إدارة مهام',
],
'pl-tech-2': [
'مقدمة في تطوير الويب و HTML', 'تنسيق الصفحات بـ CSS',
'أساسيات JavaScript', 'مقدمة في React.js',
'إدارة الحالة في React', 'بناء واجهة برمجة التطبيقات (API)',
'قواعد البيانات مع MongoDB', 'نشر التطبيق على الإنترنت',
],
'pl-tech-3': [
'ما هو الذكاء الاصطناعي؟', 'أنواع التعلم الآلي',
'الانحدار الخطي واللوجستي', 'أشجار القرار والغابات العشوائية',
'الشبكات العصبية الاصطناعية', 'التعلم العميق (Deep Learning)',
'معالجة اللغات الطبيعية (NLP)', 'الرؤية الحاسوبية (Computer Vision)',
'التعلم المعزز', 'أخلاقيات الذكاء الاصطناعي',
],
'pl-hist-1': [
'مقدمة في الفتوحات الإسلامية', 'غزوات النبي ﷺ',
'فتح مكة المكرمة', 'الفتوحات في عهد أبي بكر الصديق',
'فتح العراق وفارس', 'فتح الشام ومصر',
'معركة اليرموك', 'معركة القادسية',
'فتح فارس والأهواز', 'الفتوحات في عهد عثمان بن عفان',
'الفتوحات في أفريقيا', 'فتح الأندلس',
'معركة Tours وبلاط الشهداء', 'الفتوحات في عهد عبد الملك بن مروان',
'ميراث الفتوحات الإسلامية',
],
'pl-hist-2': [
'الفتح الإسلامي للأندلس', 'عهد الولاة في الأندلس',
'تأسيس إمارة قرطبة', 'عصر الخلافة الأموية في الأندلس',
'عصر ملوك الطوائف', 'دولة المرابطين والموحدون',
'مملكة غرناطة آخر المعاقل', 'سقوط غرناطة ومأساة التطهير',
],
'pl-hist-3': [
'خالد بن الوليد سيف الله', 'صلاح الدين الأيوبي',
'طارق بن زياد فاتح الأندلس', 'عقبة بن نافع فاتح أفريقيا',
'موسى بن نصير', 'الإمام البخاري',
'ابن سينا شيخ الأطباء', 'ابن خلدون مؤسس علم الاجتماع',
'طه حسين عميد الأدب العربي', 'الشافعي إمام الفقهاء',
'ابن تيمية شيخ الإسلام', 'محمد الفاتح فاتح القسطنطينة',
],
'pl-inv-1': [
'اختراع الطباعة وتأثيرها', 'المحرك البخاري والثورة الصناعية',
'الكهرباء وتغير العالم', 'اختراع الهاتف والاتصالات',
'الطائرة: تحقيق حلم الطيران', 'الكمبيوتر والثورة الرقمية',
'الإنترنت: الشبكة التي غيرت العالم', 'اختراع الهاتف الذكي',
'الطباعة ثلاثية الأبعاد', 'السيارة الكهربائية ومستقبل النقل',
],
'pl-inv-2': [
'مقدمة في الروبوتات و Arduino', 'المكونات الإلكترونية الأساسية',
'التحكم بالمحركات والحركة', 'المستشعرات وقراءة البيانات',
'التحكم عن بعد بالبلوتوث', 'بناء هيكل الروبوت',
'برمجة سلوك الروبوت', 'المشروع النهائي: روبوت متكامل',
],
'pl-sharia-1': [
'مقدمة في علم الفقه وأهميته', 'المياه والطهارة',
'الوضوء: أحكامه وسننه', 'التيمم والمسح على الخفين',
'الصلاة: أركانها وشروطها', 'صلاة الجماعة وصلاة الجمعة',
'صلاة المريض وقضاء الصلاة', 'الصيام: أحكامه وآدابه',
'صيام التطوع والنوافل', 'الزكاة: شروطها ومصارفها',
'زكاة الفطر والأموال', 'الحج والعمرة: المناسك',
'أحكام الجهاد في الإسلام', 'الأيمان والنذور',
'الأطعمة والأشربة في الإسلام', 'اللباس والزينة في الإسلام',
'آداب الطعام والشراب', 'آداب النوم والاستيقاظ',
'صلة الرحم وبر الوالدين', 'الأمر بالمعروف والنهي عن المنكر',
],
'pl-sharia-2': [
'مقدمة في علم العقيدة', 'أركان الإيمان الستة',
'الإيمان بالله تعالى', 'الإيمان بالملائكة',
'الإيمان بالكتب السماوية', 'الإيمان بالرسل والأنبياء',
'الإيمان باليوم الآخر', 'الإيمان بالقدر خيره وشره',
'التوحيد وأقسامه', 'الشرك وأنواعه',
'البدع والمحدثات في الدين', 'الصحابة ومنزلتهم',
'أشراط الساعة الصغرى والكبرى', 'القضاء والقدر',
'أسماء الله الحسنى وصفاته',
],
'pl-lang-1': [
'مقدمة في علم النحو وأهميته', 'أقسام الكلام: اسم وفعل وحرف',
'المعرب والمبني', 'الجملة الاسمية: المبتدأ والخبر',
'الجملة الفعلية: الفاعل والمفعول به', 'المضاف والمضاف إليه',
'النعت والتوكيد والعطف', 'الحال والتمييز',
'المفعول لأجله والمفعول معه', 'الاستثناء',
'النداء وأحكامه', 'إعراب الجمل',
],
'pl-lang-2': [
'مقدمة في علم البلاغة', 'علم البيان والتشبيه',
'الاستعارة والمجاز', 'علم المعاني والخبر والإنشاء',
'التقديم والتأخير', 'القصر والاختصاص',
'علم البديع: المحسنات اللفظية', 'علم البديع: المحسنات المعنوية',
'الإعجاز البياني للقرآن', 'تحليل بلاغي لسور مختارة',
],
'pl-biz-1': [
'من أين تبدأ فكرتك؟', 'دراسة الجدوى وتحليل السوق',
'كتابة خطة العمل (Business Plan)', 'بناء فريق العمل',
'التمويل وجمع رأس المال', 'التسويق الرقمي للمشروعات الناشئة',
'إدارة المال والميزانية', 'توسيع المشروع والنمو',
],
'pl-biz-2': [
'قصة نجاح: محمد العريفي', 'قصة نجاح: سوق.كم (Souq.com)',
'قصة نجاح: كريم (Careem)', 'قصة نجاح: نون (Noon)',
'قصة نجاح: طلبات (Talabat)', 'قصة نجاح: أنجزي (Anghami)',
],
'pl-design-1': [
'مقدمة في التصميم الهندسي', 'أساسيات الرسم الهندسي',
'الإسقاط والمساقط', 'التصميم ثلاثي الأبعاد',
'استخدام برامج التصميم CAD', 'تصميم الأبنية السكنية',
'التصميم المستدام والبيئي', 'تصميم المساجد والمراكز الإسلامية',
'التصميم الداخلي وتنسيق المساحات', 'مشروع تطبيقي: تصميم فيلا متكاملة',
],
'pl-design-2': [
'المسجد الحرام والمسجد النبوي', 'مسجد الصخرة في القدس',
'جامع قرطبة الكبير', 'مسجد السلطان أحمد في إسطنبول',
'تاج محل في الهند', 'قصر الحمراء في غرناطة',
'المدرسة المستنصرية في بغداد', 'مسجد الشيخ زايد الكبير',
],
'pl-proj-1': [
'تطبيق كريم: أول يونيكورن عربي', 'منصة بايت: ثورة في التعليم',
'شركة أرامكو: عملاق الطاقة', 'مدينة NEOM: رؤية المستقبل',
'برج المملكة: أيقونة الرياض', 'وادي السيليكون العربي',
],
'pl-cult-1': [
'الضيافة العربية: كرم لا حدود له', 'القهوة العربية وثقافة تقديمها',
'الأعياد والمناسبات العربية', 'الموسيقى والفنون العربية',
'الخط العربي وفنونه', 'الأسواق الشعبية العربية',
'الحرف اليدوية العربية', 'الطبخ العربي وتنوعه',
],
'pl-cult-2': [
'اللباس في العصر الجاهلي', 'لباس الرسول ﷺ والصحابة',
'اللباس في العصر الأموي والعباسي', 'اللباس في الأندلس',
'اللباس في العصر العثماني', 'اللباس العربي المعاصر والحجاب',
],
'pl-innov-1': [
'الابتكار في الإمارات', 'التقنية في السعودية: رؤية 2030',
'الابتكار في مصر: وادي التكنولوجيا', 'الأردن: وادي السيليكون الجديد',
'الابتكار في تونس والمغرب', 'المرأة العربية والابتكار',
'الابتكار في التعليم العربي', 'مستقبل الابتكار العربي',
],
'pl-pol-1': [
'جذور القضية الفلسطينية', 'وعد بلفور والانتداب البريطاني',
'النكبة عام 1948', 'حرب 1967 وحرب الاستنزاف',
'حرب أكتوبر 1973', 'اتفاقيات كامب ديفيد وأوسلو',
'الانتفاضة الأولى والثانية', 'حصار غزة',
'القدس ومكانتها', 'المقاومة الفلسطينية',
'القضية في المحافل الدولية', 'مستقبل القضية الفلسطينية',
],
'pl-wars-1': [
'حروب ما قبل الإسلام', 'غزوات الرسول ﷺ',
'حروب الردة', 'الفتوحات الإسلامية الكبرى',
'الحروب الصليبية', 'معركة حطين',
'فتح القسطنطينية', 'معركة عين جالوت',
'حرب الاستقلال الجزائرية', 'حرب 1948',
'حرب 1967', 'حرب 1973',
'الحرب اللبنانية الأهلية', 'الحرب العراقية الإيرانية',
'حرب الخليج الثانية',
],
'pl-wars-2': [
'خالد بن الوليد: سيف الله المسلول', 'صلاح الدين الأيوبي: محرر القدس',
'طارق بن زياد: فاتح الأندلس', 'عقبة بن نافع: فاتح أفريقيا',
'أسامة بن زيد: أصغر قائد', 'المعتصم بالله: بطل عمورية',
'قطز: بطل عين جالوت', 'محمد الفاتح: فاتح القسطنطينية',
'صلاح الدين الصفدي', 'أبو عبيدة عامر بن الجراح',
],
};
// Generate durations for each playlist
function generateDurations(count: number): number[] {
return Array.from({ length: count }, () => Math.floor(Math.random() * 30 + 8) * 60 + Math.floor(Math.random() * 60));
}
export const videos: Record<string, Video[]> = {};
for (const pl of playlists) {
const titles = videoTitlesMap[pl.id];
if (titles) {
const durations = generateDurations(titles.length);
videos[pl.id] = makeVideos(pl.channelId, pl.id, titles, durations);
}
}
// Standalone videos (not in playlists)
export const standaloneVideos: Video[] = [
{ id: 'sv-1', playlistId: '', channelId: 'ch-tech', title: 'لماذا يجب أن تتعلم البرمجة في 2024؟', description: 'في هذا الفيديو نناقش أهمية تعلم البرمجة وأفضل اللغات للبدء', duration: '18:30', durationSeconds: 1110, viewCount: 342000, publishedAt: '2024-03-15', position: 0 },
{ id: 'sv-2', playlistId: '', channelId: 'ch-tech', title: 'أفضل 10 محررات أكواد للمبرمجين', description: 'مقارنة بين أفضل محررات الأكواد ومميزات كل منها', duration: '22:15', durationSeconds: 1335, viewCount: 198000, publishedAt: '2024-03-20', position: 0 },
{ id: 'sv-3', playlistId: '', channelId: 'ch-history', title: 'كيف بنى المسلمون حضارة عظيمة؟', description: 'نظرة شاملة على عوامل قيام الحضارة الإسلامية', duration: '35:45', durationSeconds: 2145, viewCount: 567000, publishedAt: '2024-02-10', position: 0 },
{ id: 'sv-4', playlistId: '', channelId: 'ch-history', title: 'أعظم المعارك في التاريخ الإسلامي', description: 'استعراض لأعظم المعارك التي غيرت مجرى التاريخ', duration: '42:00', durationSeconds: 2520, viewCount: 421000, publishedAt: '2024-01-25', position: 0 },
{ id: 'sv-5', playlistId: '', channelId: 'ch-sharia', title: 'أهمية طلب العلم الشرعي', description: 'فضيلة طلب العلم الشرعي وأهميته في حياة المسلم', duration: '25:10', durationSeconds: 1510, viewCount: 890000, publishedAt: '2024-04-01', position: 0 },
{ id: 'sv-6', playlistId: '', channelId: 'ch-invent', title: 'اختراع عربي ي revolutionize عالم الطاقة', description: 'تعرف على اختراع عربي جديد في مجال الطاقة المتجددة', duration: '15:45', durationSeconds: 945, viewCount: 234000, publishedAt: '2024-03-28', position: 0 },
{ id: 'sv-7', playlistId: '', channelId: 'ch-lang', title: 'جمال اللغة العربية: 10 كلمات لا مثيل لها', description: 'كلمات عربية فريدة لا توجد في أي لغة أخرى', duration: '12:30', durationSeconds: 750, viewCount: 456000, publishedAt: '2024-03-10', position: 0 },
{ id: 'sv-8', playlistId: '', channelId: 'ch-biz', title: '5 مشاريع ناشئة عربية حققت نجاحًا عالميًا', description: 'قصص نجاح لمشاريع عربية وصلت للعالمية', duration: '28:20', durationSeconds: 1700, viewCount: 312000, publishedAt: '2024-02-20', position: 0 },
{ id: 'sv-9', playlistId: '', channelId: 'ch-design', title: 'التصميم المستدام: مستقبل العمارة', description: 'كيف يغير التصميم المستدام وجه العمارة العالمية', duration: '19:55', durationSeconds: 1195, viewCount: 167000, publishedAt: '2024-03-05', position: 0 },
{ id: 'sv-10', playlistId: '', channelId: 'ch-culture', title: 'الخط العربي: فن وتاريخ', description: 'تاريخ فن الخط العربي وأشهر الخطاطين', duration: '32:10', durationSeconds: 1930, viewCount: 278000, publishedAt: '2024-01-30', position: 0 },
{ id: 'sv-11', playlistId: '', channelId: 'ch-politics', title: 'النظام السياسي العربي: تحليل شامل', description: 'تحليل معمق للأنظمة السياسية في الدول العربية', duration: '45:30', durationSeconds: 2730, viewCount: 534000, publishedAt: '2024-02-15', position: 0 },
{ id: 'sv-12', playlistId: '', channelId: 'ch-wars', title: 'أعظم 10 قادة عسكريين في التاريخ', description: 'ترتيب أعظم القادة العسكريين عبر التاريخ', duration: '38:20', durationSeconds: 2300, viewCount: 678000, publishedAt: '2024-03-22', position: 0 },
];
// ── Default Folders ───────────────────────────────────────
export const defaultFolders: Folder[] = [
{
id: 'folder-1',
name: 'البرمجة والتقنية',
color: 'blue',
emoji: '💻',
channelIds: ['ch-tech', 'ch-invent', 'ch-innovation'],
},
{
id: 'folder-2',
name: 'العلوم الشرعية',
color: 'purple',
emoji: '📖',
channelIds: ['ch-sharia', 'ch-culture'],
},
{
id: 'folder-3',
name: 'التاريخ والحروب',
color: 'amber',
emoji: '⚔️',
channelIds: ['ch-history', 'ch-wars'],
},
{
id: 'folder-4',
name: 'اللغة والأدب',
color: 'rose',
emoji: '✨',
channelIds: ['ch-lang'],
},
{
id: 'folder-5',
name: 'ريادة الأعمال',
color: 'orange',
emoji: '🚀',
channelIds: ['ch-biz', 'ch-projects'],
},
];
// ── Helper functions ──────────────────────────────────────
export function getChannel(id: string): Channel | undefined {
return channels.find(c => c.id === id);
}
export function getPlaylist(id: string): Playlist | undefined {
return playlists.find(p => p.id === id);
}
export function getChannelPlaylists(channelId: string): Playlist[] {
return playlists.filter(p => p.channelId === channelId);
}
export function getPlaylistVideos(playlistId: string): Video[] {
return videos[playlistId] || [];
}
export function getChannelVideos(channelId: string): Video[] {
const allVideos: Video[] = [];
for (const pl of playlists.filter(p => p.channelId === channelId)) {
allVideos.push(...(videos[pl.id] || []));
}
allVideos.push(...standaloneVideos.filter(v => v.channelId === channelId));
return allVideos;
}
export function formatNumber(num: number): string {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(0) + 'K';
return num.toString();
}
export function getCategoryColor(cat: string): string {
const map: Record<string, string> = {
'تعليم': 'bg-blue-100 text-blue-700',
'تقنية': 'bg-cyan-100 text-cyan-700',
'برمجة': 'bg-indigo-100 text-indigo-700',
'تاريخ': 'bg-amber-100 text-amber-700',
'إسلام': 'bg-green-100 text-green-700',
'حروب': 'bg-red-100 text-red-700',
'اختراعات': 'bg-emerald-100 text-emerald-700',
'دين': 'bg-purple-100 text-purple-700',
'شريعة': 'bg-violet-100 text-violet-700',
'لغة عربية': 'bg-pink-100 text-pink-700',
'ريادة': 'bg-orange-100 text-orange-700',
'تصميم': 'bg-fuchsia-100 text-fuchsia-700',
'ثقافة': 'bg-rose-100 text-rose-700',
'سياسة': 'bg-slate-100 text-slate-700',
'ابتكار': 'bg-teal-100 text-teal-700',
'مشاريع': 'bg-sky-100 text-sky-700',
};
return map[cat] || 'bg-gray-100 text-gray-700';
}
+116
View File
@@ -0,0 +1,116 @@
@import "tailwindcss";
@theme {
--font-family-tajawal: 'Tajawal', sans-serif;
--color-primary: #FF6B35;
--color-primary-dark: #E55A2B;
--color-secondary: #7C3AED;
--color-accent: #10B981;
--color-accent-gold: #F59E0B;
--color-surface: #FFF8F0;
--color-surface-card: #FFFFFF;
--color-surface-dark: #1E1B2E;
}
* {
font-family: 'Tajawal', sans-serif;
}
body {
background-color: #FFF8F0;
overflow: hidden;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 5px rgba(255, 107, 53, 0.3); }
50% { box-shadow: 0 0 20px rgba(255, 107, 53, 0.6); }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes progress-fill {
from { width: 0%; }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animate-shimmer {
animation: shimmer 2s infinite;
background-size: 200% 100%;
}
.animate-pulse-glow {
animation: pulse-glow 2s infinite;
}
.animate-slide-up {
animation: slide-up 0.4s ease-out;
}
.animate-slide-in {
animation: slide-in-right 0.3s ease-out;
}
.animate-progress {
animation: progress-fill 1s ease-out;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.gradient-animated {
background-size: 200% 200%;
animation: gradient-shift 3s ease infinite;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.2);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(124, 58, 237, 0.4);
}
.video-card:hover .video-overlay {
opacity: 1;
}
.progress-ring {
transform: rotate(-90deg);
}
.playlist-gradient {
background: linear-gradient(135deg, #FF6B35 0%, #7C3AED 50%, #10B981 100%);
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
+192
View File
@@ -0,0 +1,192 @@
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import { ViewType, Folder, UserProgress } from './types';
import { defaultFolders } from './data';
interface AppState {
currentView: ViewType;
followedChannelIds: string[];
folders: Folder[];
progress: UserProgress;
sidebarOpen: boolean;
searchQuery: string;
}
interface AppContextType extends AppState {
setView: (view: ViewType) => void;
followChannel: (id: string) => void;
unfollowChannel: (id: string) => void;
toggleSidebar: () => void;
setSearchQuery: (q: string) => void;
markVideoWatched: (videoId: string) => void;
updatePlaylistProgress: (playlistId: string, videoIndex: number) => void;
completePlaylist: (playlistId: string) => void;
createFolder: (name: string, color: string, emoji: string) => void;
deleteFolder: (id: string) => void;
addChannelToFolder: (folderId: string, channelId: string) => void;
removeChannelFromFolder: (folderId: string, channelId: string) => void;
updateFolder: (folderId: string, updates: Partial<Folder>) => void;
}
const AppContext = createContext<AppContextType | null>(null);
export function useApp() {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('useApp must be used within AppProvider');
return ctx;
}
const STORAGE_KEY = 'masari-state';
function loadState(): Partial<AppState> {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) return JSON.parse(saved);
} catch {}
return {};
}
function saveState(state: AppState) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
followedChannelIds: state.followedChannelIds,
folders: state.folders,
progress: state.progress,
}));
} catch {}
}
export function AppProvider({ children }: { children: React.ReactNode }) {
const saved = loadState();
const [currentView, setCurrentView] = useState<ViewType>({ type: 'dashboard' });
const [followedChannelIds, setFollowedChannelIds] = useState<string[]>(
saved.followedChannelIds || ['ch-tech', 'ch-history', 'ch-sharia', 'ch-lang', 'ch-biz']
);
const [folders, setFolders] = useState<Folder[]>(
saved.folders || defaultFolders
);
const [progress, setProgress] = useState<UserProgress>(
saved.progress || {
watchedVideos: [],
playlistProgress: {},
completedPlaylists: [],
streak: 3,
lastWatchDate: new Date().toISOString().split('T')[0],
totalWatched: 0,
}
);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
// Save to localStorage on changes
useEffect(() => {
saveState({ followedChannelIds, folders, progress } as AppState);
}, [followedChannelIds, folders, progress]);
const setView = useCallback((view: ViewType) => {
setCurrentView(view);
}, []);
const followChannel = useCallback((id: string) => {
setFollowedChannelIds(prev => prev.includes(id) ? prev : [...prev, id]);
}, []);
const unfollowChannel = useCallback((id: string) => {
setFollowedChannelIds(prev => prev.filter(c => c !== id));
}, []);
const toggleSidebar = useCallback(() => {
setSidebarOpen(prev => !prev);
}, []);
const markVideoWatched = useCallback((videoId: string) => {
setProgress(prev => ({
...prev,
watchedVideos: prev.watchedVideos.includes(videoId)
? prev.watchedVideos
: [...prev.watchedVideos, videoId],
totalWatched: prev.totalWatched + 1,
lastWatchDate: new Date().toISOString().split('T')[0],
}));
}, []);
const updatePlaylistProgress = useCallback((playlistId: string, videoIndex: number) => {
setProgress(prev => ({
...prev,
playlistProgress: {
...prev.playlistProgress,
[playlistId]: Math.max(prev.playlistProgress[playlistId] || 0, videoIndex),
},
}));
}, []);
const completePlaylist = useCallback((playlistId: string) => {
setProgress(prev => ({
...prev,
completedPlaylists: prev.completedPlaylists.includes(playlistId)
? prev.completedPlaylists
: [...prev.completedPlaylists, playlistId],
}));
}, []);
const createFolder = useCallback((name: string, color: string, emoji: string) => {
const newFolder: Folder = {
id: `folder-${Date.now()}`,
name,
color,
emoji,
channelIds: [],
};
setFolders(prev => [...prev, newFolder]);
}, []);
const deleteFolder = useCallback((id: string) => {
setFolders(prev => prev.filter(f => f.id !== id));
}, []);
const addChannelToFolder = useCallback((folderId: string, channelId: string) => {
setFolders(prev => prev.map(f =>
f.id === folderId && !f.channelIds.includes(channelId)
? { ...f, channelIds: [...f.channelIds, channelId] }
: f
));
}, []);
const removeChannelFromFolder = useCallback((folderId: string, channelId: string) => {
setFolders(prev => prev.map(f =>
f.id === folderId
? { ...f, channelIds: f.channelIds.filter(c => c !== channelId) }
: f
));
}, []);
const updateFolder = useCallback((folderId: string, updates: Partial<Folder>) => {
setFolders(prev => prev.map(f =>
f.id === folderId ? { ...f, ...updates } : f
));
}, []);
const value: AppContextType = {
currentView,
followedChannelIds,
folders,
progress,
sidebarOpen,
searchQuery,
setView,
followChannel,
unfollowChannel,
toggleSidebar,
setSearchQuery,
markVideoWatched,
updatePlaylistProgress,
completePlaylist,
createFolder,
deleteFolder,
addChannelToFolder,
removeChannelFromFolder,
updateFolder,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
+88
View File
@@ -0,0 +1,88 @@
export interface Channel {
id: string;
name: string;
description: string;
gradient: string;
emoji: string;
categories: string[];
subscriberCount: number;
videoCount: number;
}
export interface Playlist {
id: string;
channelId: string;
title: string;
description: string;
videoCount: number;
gradient: string;
emoji: string;
categories: string[];
}
export interface Video {
id: string;
playlistId: string;
channelId: string;
title: string;
description: string;
duration: string;
durationSeconds: number;
viewCount: number;
publishedAt: string;
position: number;
}
export interface Folder {
id: string;
name: string;
color: string;
emoji: string;
channelIds: string[];
}
export interface UserProgress {
watchedVideos: string[];
playlistProgress: Record<string, number>;
completedPlaylists: string[];
streak: number;
lastWatchDate: string;
totalWatched: number;
}
export type ViewType =
| { type: 'dashboard' }
| { type: 'channel'; channelId: string }
| { type: 'playlist'; playlistId: string }
| { type: 'browser' }
| { type: 'folder'; folderId: string }
| { type: 'video'; videoId: string; playlistId?: string };
export const CONTENT_FILTER = {
blockedKeywords: [
'sex', 'porn', 'nude', 'xxx', 'adult', 'erotic', 'nsfw',
'مخلة', 'جنس', 'إباحي', 'عارية', 'عري', 'خمر', 'مخدرات',
'قمار', 'رذيلة', 'فاحش', 'عاهرة', 'لواط', 'سحاق',
'elusive', 'twilight', 'vampire'
],
allowedCategories: [
'تعليم', 'تقنية', 'برمجة', 'علوم', 'دين', 'إسلام', 'تاريخ',
'اختراعات', 'تصميم', 'مشاريع', 'ريادة', 'أعمال', 'شريعة',
'لغة عربية', 'ثقافة', 'سياسة', 'حروب', 'فقه', 'عقيدة',
'تفسير', 'حديث', 'سيرة', 'قرآن', 'ذكاء اصطناعي', 'هندسة',
'عمارة', 'فيزياء', 'كيمياء', 'رياضيات', 'طب', 'فلك',
'ابتكار', 'شعر', 'نثر', 'أناشيد', 'أدب', 'بلاغة', 'نحو',
'صرف', 'حضارة', 'تراث', 'فن إسلامي', 'خط عربي',
'روبوتات', 'إلكترونيات', 'طاقة', 'بيئة', 'زراعة',
'إدارة', 'تسويق', 'اقتصاد', 'محاسبة', 'قانون',
'عسكرية', 'استراتيجية', 'جغرافيا', 'آثار'
]
};
export function isContentAllowed(title: string, _categories: string[]): boolean {
const lowerTitle = title.toLowerCase();
for (const keyword of CONTENT_FILTER.blockedKeywords) {
if (lowerTitle.includes(keyword.toLowerCase())) return false;
}
return true;
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["node"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
+19
View File
@@ -0,0 +1,19 @@
import path from "path";
import { fileURLToPath } from "url";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(), viteSingleFile()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
});