canina/frontend/application/components/VideosPage.tsx
parsa aghaei 813c96742d
Some checks failed
Deploy Canina / deploy (push) Has been cancelled
feat(doctors): add doctors management, image optimization, testimonials section and clean fallbacks
2026-08-17 16:25:20 +03:30

160 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ChevronRight, ChevronLeft, PlayCircle, ShieldCheck, X, Search, Clock, User } from "lucide-react";
import SafeImage from "./SafeImage";
import { videoService, Video } from "../lib/services/videoService";
import { useRouter } from 'next/navigation';
import VideoModalPlayer from "./VideoModalPlayer";
export default function VideosPage() {
const router = useRouter();
const [videos, setVideos] = useState<Video[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
const [search, setSearch] = useState("");
React.useEffect(() => {
setIsLoading(true);
videoService.getVideos({ limit: 100 })
.then((data) => {
if (data && Array.isArray(data)) {
setVideos(data);
} else {
setVideos([]);
}
})
.catch((err) => {
console.error('Failed to load videos:', err);
setVideos([]);
})
.finally(() => {
setIsLoading(false);
});
}, []);
const filteredVideos = videos.filter(v =>
(v.title || '').toLowerCase().includes(search.toLowerCase()) ||
(v.doctor || '').toLowerCase().includes(search.toLowerCase()) ||
(v.description || '').toLowerCase().includes(search.toLowerCase())
);
return (
<div className="min-h-screen bg-medical-gray-900 pt-8 pb-20 px-4 font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex flex-col md:flex-row items-center justify-between mb-16 gap-6">
<div className="text-right">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-canina-blue/10 text-canina-blue rounded-full text-[10px] font-black uppercase tracking-widest mb-4">
<PlayCircle className="w-3 h-3" />
Video Academy
</div>
<h1 className="text-4xl lg:text-6xl font-black text-white italic leading-tight">
آکادمی ویدئویی <span className="text-canina-blue">کنینا</span>
</h1>
</div>
<button
onClick={() => router.push('/')}
className="px-8 py-3 bg-white/5 border border-white/10 rounded-2xl text-white shadow-sm flex items-center gap-2 font-black hover:bg-white/10 transition-all font-vazir"
>
<span>بازگشت</span>
<ChevronLeft className="w-5 h-5" />
</button>
</div>
{/* Search Bar */}
<div className="mb-12 relative max-w-2xl">
<Search className="absolute right-6 top-1/2 -translate-y-1/2 text-white/30 w-5 h-5" />
<input
type="text"
placeholder="جستجو در ویدئوهای آموزشی..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-[2rem] py-5 pr-14 pl-6 text-white outline-none focus:bg-white/10 transition-all font-bold"
/>
</div>
{/* Video Grid or Skeleton */}
{isLoading ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="animate-pulse space-y-4">
<div className="aspect-video bg-white/5 rounded-[2.5rem] border border-white/5" />
<div className="h-6 bg-white/5 rounded-xl w-3/4" />
<div className="h-4 bg-white/5 rounded-xl w-1/2" />
</div>
))}
</div>
) : filteredVideos.length === 0 ? (
<div className="bg-white/5 border border-white/10 rounded-[3rem] p-16 text-center text-white/50">
<PlayCircle className="w-16 h-16 mx-auto mb-4 opacity-30 text-canina-blue" />
<h3 className="text-xl font-bold text-white mb-2">ویدئویی یافت نشد</h3>
<p className="text-sm font-medium">هیچ ویدئویی مطابق با جستجوی شما در آکادمی وجود ندارد.</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredVideos.map((video, idx) => (
<motion.div
key={video.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.1 }}
className="group cursor-pointer"
onClick={() => {
setSelectedVideo(video);
videoService.incrementViews(video.id);
}}
>
<div className="relative aspect-video rounded-[2.5rem] overflow-hidden mb-6 shadow-2xl border border-white/5 bg-black/40">
<SafeImage
src={video.thumbnail}
alt={video.title}
className="w-full h-full"
imgClassName="object-cover group-hover:scale-110 transition-transform duration-700 opacity-60 group-hover:opacity-100"
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-16 h-16 bg-canina-blue rounded-full flex items-center justify-center text-white shadow-2xl group-hover:scale-110 transition-transform">
<PlayCircle className="w-10 h-10" />
</div>
</div>
{video.duration && (
<div className="absolute bottom-4 right-4 bg-black/60 backdrop-blur-md text-white px-3 py-1 rounded-lg text-[10px] font-black">
{video.duration}
</div>
)}
</div>
<div className="px-4">
<h3 className="text-xl font-black text-white group-hover:text-canina-blue transition-colors mb-2 leading-tight">
{video.title}
</h3>
<div className="flex items-center gap-4 text-xs font-bold text-white/40">
<div className="flex items-center gap-1">
<User className="w-3 h-3" />
{video.doctor || 'کادر علمی کنینا'}
</div>
{video.duration && (
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{video.duration}</span>
</div>
)}
</div>
</div>
</motion.div>
))}
</div>
)}
</div>
{/* Custom Feature-Rich Video Player Modal */}
<VideoModalPlayer
isOpen={!!selectedVideo}
onClose={() => setSelectedVideo(null)}
video={selectedVideo}
/>
</div>
);
}