418 lines
18 KiB
TypeScript
418 lines
18 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import { Plus, Search, Edit2, Trash2, Video as VideoIcon, Star, CheckCircle, X, Upload, Image as ImageIcon, Film, Code } from 'lucide-react';
|
||
import api from '../services/api';
|
||
|
||
interface Video {
|
||
id: string;
|
||
title: string;
|
||
doctor: string;
|
||
duration: string;
|
||
thumbnail: string;
|
||
videoUrl: string;
|
||
description: string;
|
||
viewsCount: number;
|
||
isFeatured: boolean;
|
||
createdAt: string;
|
||
}
|
||
|
||
export default function Videos() {
|
||
const [videos, setVideos] = useState<Video[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [editingVideo, setEditingVideo] = useState<Video | null>(null);
|
||
const [isUploadingThumbnail, setIsUploadingThumbnail] = useState(false);
|
||
const [isUploadingVideo, setIsUploadingVideo] = useState(false);
|
||
|
||
const thumbnailInputRef = useRef<HTMLInputElement>(null);
|
||
const videoInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
const [formData, setFormData] = useState({
|
||
title: '',
|
||
doctor: '',
|
||
duration: '۰۲:۰۰',
|
||
thumbnail: '',
|
||
videoUrl: '',
|
||
description: '',
|
||
isFeatured: false,
|
||
});
|
||
|
||
const fetchVideos = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
const res = await api.get('/videos', {
|
||
params: { search: searchTerm, limit: 100 },
|
||
});
|
||
setVideos(res.data.data || res.data || []);
|
||
} catch (err) {
|
||
console.error('Failed to fetch videos:', err);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchVideos();
|
||
}, [searchTerm]);
|
||
|
||
const handleOpenModal = (video?: Video) => {
|
||
if (video) {
|
||
setEditingVideo(video);
|
||
setFormData({
|
||
title: video.title,
|
||
doctor: video.doctor,
|
||
duration: video.duration || '۰۲:۰۰',
|
||
thumbnail: video.thumbnail || '',
|
||
videoUrl: video.videoUrl,
|
||
description: video.description || '',
|
||
isFeatured: Boolean(video.isFeatured),
|
||
});
|
||
} else {
|
||
setEditingVideo(null);
|
||
setFormData({
|
||
title: '',
|
||
doctor: '',
|
||
duration: '۰۲:۰۰',
|
||
thumbnail: '',
|
||
videoUrl: '',
|
||
description: '',
|
||
isFeatured: false,
|
||
});
|
||
}
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const handleFileUpload = async (file: File, type: 'thumbnail' | 'video') => {
|
||
const data = new FormData();
|
||
data.append('file', file);
|
||
|
||
if (type === 'thumbnail') setIsUploadingThumbnail(true);
|
||
if (type === 'video') setIsUploadingVideo(true);
|
||
|
||
try {
|
||
const res = await api.post('/cms/upload', data, {
|
||
headers: { 'Content-Type': 'multipart/form-data' },
|
||
});
|
||
const fileUrl = res.data.url || res.data.fileUrl;
|
||
if (type === 'thumbnail') {
|
||
setFormData(prev => ({ ...prev, thumbnail: fileUrl }));
|
||
} else {
|
||
setFormData(prev => ({ ...prev, videoUrl: fileUrl }));
|
||
}
|
||
} catch (err) {
|
||
console.error('Upload failed:', err);
|
||
alert('خطا در آپلود فایل');
|
||
} finally {
|
||
setIsUploadingThumbnail(false);
|
||
setIsUploadingVideo(false);
|
||
}
|
||
};
|
||
|
||
const handleSave = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
if (editingVideo) {
|
||
await api.put(`/videos/${editingVideo.id}`, formData);
|
||
} else {
|
||
await api.post('/videos', formData);
|
||
}
|
||
setIsModalOpen(false);
|
||
fetchVideos();
|
||
} catch (err) {
|
||
console.error('Failed to save video:', err);
|
||
alert('خطا در ذخیرهسازی ویدئو');
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id: string) => {
|
||
if (!window.confirm('آیا از حذف این ویدئوی آموزشی اطمینان دارید؟')) return;
|
||
try {
|
||
await api.delete(`/videos/${id}`);
|
||
fetchVideos();
|
||
} catch (err) {
|
||
console.error('Failed to delete video:', err);
|
||
alert('خطا در حذف ویدئو');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6 font-vazir text-right" dir="rtl">
|
||
{/* Header */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-white p-6 rounded-2xl border border-gray-100 shadow-xs">
|
||
<div>
|
||
<h1 className="text-2xl font-black text-gray-900 flex items-center gap-3">
|
||
<VideoIcon className="w-7 h-7 text-purple-600" />
|
||
مدیریت ویدئوها و آکادمی آموزشی
|
||
</h1>
|
||
<p className="text-sm text-gray-500 font-bold mt-1">
|
||
آپلود مستقیم ویدئو/کاور یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کانینا
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => handleOpenModal()}
|
||
className="bg-purple-600 hover:bg-purple-700 text-white font-bold px-5 py-3 rounded-xl flex items-center justify-center gap-2 transition-all shadow-md"
|
||
>
|
||
<Plus className="w-5 h-5" />
|
||
افزودن ویدئوی جدید
|
||
</button>
|
||
</div>
|
||
|
||
{/* Search Bar */}
|
||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4">
|
||
<Search className="w-5 h-5 text-gray-400 shrink-0" />
|
||
<input
|
||
type="text"
|
||
placeholder="جستجو بر اساس عنوان یا نام پزشک..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full bg-transparent border-none text-sm focus:outline-none font-bold"
|
||
/>
|
||
</div>
|
||
|
||
{/* Videos Grid / Table */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-xs">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-right border-collapse">
|
||
<thead>
|
||
<tr className="bg-gray-50 text-gray-500 text-xs font-black uppercase tracking-wider border-b border-gray-100">
|
||
<th className="py-4 px-6">کاور & عنوان</th>
|
||
<th className="py-4 px-6">دکتر / ارائهدهنده</th>
|
||
<th className="py-4 px-6">زمان</th>
|
||
<th className="py-4 px-6">نمایش ویژه (صفحه اصلی)</th>
|
||
<th className="py-4 px-6 text-center">عملیات ادمین</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100 text-sm font-bold text-gray-700">
|
||
{isLoading ? (
|
||
<tr>
|
||
<td colSpan={5} className="py-12 text-center text-gray-400">
|
||
در حال دریافت لیست ویدئوها...
|
||
</td>
|
||
</tr>
|
||
) : videos.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={5} className="py-12 text-center text-gray-400">
|
||
هیچ ویدئویی ثبت نشده است.
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
videos.map((video) => (
|
||
<tr key={video.id} className="hover:bg-gray-50/50 transition-colors">
|
||
<td className="py-4 px-6">
|
||
<div className="flex items-center gap-4">
|
||
<img
|
||
src={video.thumbnail || 'https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=200'}
|
||
alt={video.title}
|
||
className="w-16 h-10 object-cover rounded-lg border border-gray-200 shrink-0"
|
||
/>
|
||
<div>
|
||
<h4 className="font-black text-gray-900 leading-snug">{video.title}</h4>
|
||
<p className="text-xs text-gray-400 line-clamp-1 mt-0.5">{video.description}</p>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="py-4 px-6 font-black text-purple-700">{video.doctor}</td>
|
||
<td className="py-4 px-6 font-mono dir-ltr text-right">{video.duration}</td>
|
||
<td className="py-4 px-6">
|
||
{video.isFeatured ? (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-amber-50 text-amber-700 border border-amber-200 rounded-full text-xs font-black">
|
||
<Star className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />
|
||
ویژه (صفحه اصلی)
|
||
</span>
|
||
) : (
|
||
<span className="text-xs text-gray-400">عادی</span>
|
||
)}
|
||
</td>
|
||
<td className="py-4 px-6">
|
||
<div className="flex items-center justify-center gap-2">
|
||
<button
|
||
onClick={() => handleOpenModal(video)}
|
||
className="p-2 hover:bg-purple-50 text-purple-600 rounded-xl transition-colors"
|
||
title="ویرایش"
|
||
>
|
||
<Edit2 className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDelete(video.id)}
|
||
className="p-2 hover:bg-red-50 text-red-600 rounded-xl transition-colors"
|
||
title="حذف"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Create / Edit Modal */}
|
||
{isModalOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
|
||
<div className="bg-white w-full max-w-xl rounded-3xl p-6 sm:p-8 space-y-6 shadow-2xl relative border border-gray-100 max-h-[90vh] overflow-y-auto">
|
||
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
|
||
<h3 className="text-xl font-black text-gray-900">
|
||
{editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید'}
|
||
</h3>
|
||
<button
|
||
onClick={() => setIsModalOpen(false)}
|
||
className="w-9 h-9 flex items-center justify-center rounded-xl hover:bg-gray-100 text-gray-400 hover:text-gray-700"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSave} className="space-y-4">
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-black text-gray-700 block">عنوان ویدئو</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
value={formData.title}
|
||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||
placeholder="مثلاً: نحوه آمادهسازی کانیهیدروکس GAG"
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 font-bold"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-black text-gray-700 block">نام دکتر / ارائهدهنده</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
value={formData.doctor}
|
||
onChange={(e) => setFormData({ ...formData, doctor: e.target.value })}
|
||
placeholder="دکتر کلاوس هنینگ"
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 font-bold"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-black text-gray-700 block">مدت زمان (مثلاً ۰۲:۳۰)</label>
|
||
<input
|
||
type="text"
|
||
value={formData.duration}
|
||
onChange={(e) => setFormData({ ...formData, duration: e.target.value })}
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 font-bold"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Cover Thumbnail with Upload */}
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-black text-gray-700 block">تصویر کاور (لینک یا آپلود مستقیم)</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={formData.thumbnail}
|
||
onChange={(e) => setFormData({ ...formData, thumbnail: e.target.value })}
|
||
placeholder="https://... یا آپلود"
|
||
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 font-bold"
|
||
/>
|
||
<input
|
||
type="file"
|
||
ref={thumbnailInputRef}
|
||
accept="image/*"
|
||
className="hidden"
|
||
onChange={(e) => {
|
||
if (e.target.files?.[0]) handleFileUpload(e.target.files[0], 'thumbnail');
|
||
}}
|
||
/>
|
||
<button
|
||
type="button"
|
||
disabled={isUploadingThumbnail}
|
||
onClick={() => thumbnailInputRef.current?.click()}
|
||
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200"
|
||
>
|
||
<Upload className="w-4 h-4" />
|
||
<span>{isUploadingThumbnail ? 'در حال آپلود...' : 'آپلود عکس'}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Video URL or Embed Tag */}
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-black text-gray-700 block">آدرس فایل ویدئو MP4 یا کد آیفرم آپارات/یوتیوب</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
required
|
||
value={formData.videoUrl}
|
||
onChange={(e) => setFormData({ ...formData, videoUrl: e.target.value })}
|
||
placeholder="https://... یا کد آیفرم <iframe...>"
|
||
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 font-bold"
|
||
/>
|
||
<input
|
||
type="file"
|
||
ref={videoInputRef}
|
||
accept="video/*"
|
||
className="hidden"
|
||
onChange={(e) => {
|
||
if (e.target.files?.[0]) handleFileUpload(e.target.files[0], 'video');
|
||
}}
|
||
/>
|
||
<button
|
||
type="button"
|
||
disabled={isUploadingVideo}
|
||
onClick={() => videoInputRef.current?.click()}
|
||
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200"
|
||
>
|
||
<Film className="w-4 h-4" />
|
||
<span>{isUploadingVideo ? 'در حال آپلود...' : 'آپلود ویدئو'}</span>
|
||
</button>
|
||
</div>
|
||
<p className="text-[10px] text-gray-400 font-bold">
|
||
میتوانید مستقیم لینک MP4 وارد کنید یا کد آیفرم (Embed Code) آپارات/یوتیوب قرار دهید.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-black text-gray-700 block">توضیحات تکمیلی</label>
|
||
<textarea
|
||
value={formData.description}
|
||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 h-24 font-bold"
|
||
placeholder="توضیحات مختصر در مورد محتوای ویدئو..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 p-4 bg-amber-50 rounded-xl border border-amber-200">
|
||
<input
|
||
type="checkbox"
|
||
id="isFeaturedCheck"
|
||
checked={formData.isFeatured}
|
||
onChange={(e) => setFormData({ ...formData, isFeatured: e.target.checked })}
|
||
className="w-4 h-4 text-amber-600 rounded focus:ring-amber-500 cursor-pointer"
|
||
/>
|
||
<label htmlFor="isFeaturedCheck" className="text-xs font-black text-amber-900 cursor-pointer">
|
||
نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت
|
||
</label>
|
||
</div>
|
||
|
||
<div className="pt-4 flex justify-end gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsModalOpen(false)}
|
||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold text-sm hover:bg-gray-50"
|
||
>
|
||
انصراف
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold text-sm hover:bg-purple-700 shadow-md"
|
||
>
|
||
ذخیره اطلاعات
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|