canina/frontend/admin-panel/src/pages/Videos.tsx

418 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}