Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae2de982fd | ||
|
|
3fe491ad1c | ||
|
|
faf8eb0619 | ||
|
|
9e9ac65c2e | ||
|
|
7a869f0e67 | ||
|
|
b45d2c5c0a | ||
|
|
9e10e16a66 | ||
|
|
96b215daaf | ||
|
|
848d9d7397 |
+14
@@ -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>
|
||||||
Generated
+2573
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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%);
|
||||||
|
}
|
||||||
@@ -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
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user