606 lines
25 KiB
TypeScript
606 lines
25 KiB
TypeScript
"use client";
|
||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import {
|
||
Play,
|
||
Pause,
|
||
RotateCcw,
|
||
RotateCw,
|
||
Volume2,
|
||
VolumeX,
|
||
Maximize,
|
||
Minimize,
|
||
Download,
|
||
Share2,
|
||
X,
|
||
Check,
|
||
ChevronDown,
|
||
ChevronUp,
|
||
Sparkles,
|
||
Loader2
|
||
} from "lucide-react";
|
||
import { getMediaUrl } from "../lib/media";
|
||
import DOMPurify from "dompurify";
|
||
|
||
interface VideoModalPlayerProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
video: {
|
||
id?: string;
|
||
title?: string;
|
||
doctor?: string;
|
||
videoUrl?: string;
|
||
thumbnail?: string;
|
||
description?: string;
|
||
quote?: string;
|
||
} | null;
|
||
}
|
||
|
||
const PLAYBACK_RATES = [0.5, 1, 1.25, 1.5, 1.75, 2, 2.5];
|
||
|
||
export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalPlayerProps) {
|
||
const videoRef = useRef<HTMLVideoElement>(null);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const rateMenuRef = useRef<HTMLDivElement>(null);
|
||
const controlsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
||
const [isPlaying, setIsPlaying] = useState(false);
|
||
const [currentTime, setCurrentTime] = useState(0);
|
||
const [duration, setDuration] = useState(0);
|
||
const [bufferedEnd, setBufferedEnd] = useState(0);
|
||
const [volume, setVolume] = useState(1);
|
||
const [isMuted, setIsMuted] = useState(false);
|
||
const [playbackRate, setPlaybackRate] = useState(1);
|
||
const [showRateMenu, setShowRateMenu] = useState(false);
|
||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||
const [isBuffering, setIsBuffering] = useState(true);
|
||
const [showControls, setShowControls] = useState(true);
|
||
const [isExpandedDesc, setIsExpandedDesc] = useState(false);
|
||
const [isCopied, setIsCopied] = useState(false);
|
||
|
||
// Close rate menu when clicking outside
|
||
useEffect(() => {
|
||
function handleClickOutside(e: MouseEvent) {
|
||
if (rateMenuRef.current && !rateMenuRef.current.contains(e.target as Node)) {
|
||
setShowRateMenu(false);
|
||
}
|
||
}
|
||
if (showRateMenu) {
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
}
|
||
return () => {
|
||
document.removeEventListener("mousedown", handleClickOutside);
|
||
};
|
||
}, [showRateMenu]);
|
||
|
||
// Reset / Restore states on open/close
|
||
useEffect(() => {
|
||
if (isOpen && video?.id) {
|
||
const savedTime = localStorage.getItem(`video_time_${video.id}`);
|
||
const initialTime = savedTime ? parseFloat(savedTime) : 0;
|
||
|
||
setIsPlaying(true);
|
||
setShowControls(true);
|
||
setIsExpandedDesc(false);
|
||
setShowRateMenu(false);
|
||
setBufferedEnd(0);
|
||
|
||
if (videoRef.current && initialTime > 0) {
|
||
videoRef.current.currentTime = initialTime;
|
||
setCurrentTime(initialTime);
|
||
}
|
||
} else if (!isOpen && video?.id) {
|
||
if (currentTime > 0) {
|
||
localStorage.setItem(`video_time_${video.id}`, currentTime.toString());
|
||
}
|
||
setIsPlaying(false);
|
||
setBufferedEnd(0);
|
||
}
|
||
}, [isOpen, video?.id]);
|
||
|
||
// Save playback time periodically
|
||
useEffect(() => {
|
||
if (video?.id && currentTime > 0) {
|
||
localStorage.setItem(`video_time_${video.id}`, currentTime.toString());
|
||
}
|
||
}, [currentTime, video?.id]);
|
||
|
||
// Update buffered track
|
||
const updateBuffer = useCallback(() => {
|
||
if (videoRef.current && videoRef.current.buffered.length > 0) {
|
||
const current = videoRef.current.currentTime;
|
||
for (let i = 0; i < videoRef.current.buffered.length; i++) {
|
||
if (
|
||
videoRef.current.buffered.start(i) <= current &&
|
||
videoRef.current.buffered.end(i) >= current
|
||
) {
|
||
setBufferedEnd(videoRef.current.buffered.end(i));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
// Handle auto-hide timer for controls
|
||
const resetControlsTimer = useCallback(() => {
|
||
setShowControls(true);
|
||
if (controlsTimeoutRef.current) {
|
||
clearTimeout(controlsTimeoutRef.current);
|
||
}
|
||
// If video is playing, auto-hide after 3.5s; if paused, stay visible
|
||
if (isPlaying) {
|
||
controlsTimeoutRef.current = setTimeout(() => {
|
||
if (!showRateMenu) {
|
||
setShowControls(false);
|
||
}
|
||
}, 3500);
|
||
}
|
||
}, [isPlaying, showRateMenu]);
|
||
|
||
useEffect(() => {
|
||
if (!isPlaying) {
|
||
setShowControls(true);
|
||
if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current);
|
||
} else {
|
||
resetControlsTimer();
|
||
}
|
||
return () => {
|
||
if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current);
|
||
};
|
||
}, [isPlaying, resetControlsTimer]);
|
||
|
||
const togglePlay = () => {
|
||
if (!videoRef.current) return;
|
||
if (videoRef.current.paused) {
|
||
videoRef.current.play().catch(console.error);
|
||
setIsPlaying(true);
|
||
} else {
|
||
videoRef.current.pause();
|
||
setIsPlaying(false);
|
||
setShowControls(true);
|
||
}
|
||
};
|
||
|
||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const time = Number(e.target.value);
|
||
if (videoRef.current) {
|
||
videoRef.current.currentTime = time;
|
||
setCurrentTime(time);
|
||
}
|
||
};
|
||
|
||
const skipTime = (seconds: number) => {
|
||
if (videoRef.current) {
|
||
videoRef.current.currentTime = Math.max(0, Math.min(videoRef.current.duration || 0, videoRef.current.currentTime + seconds));
|
||
resetControlsTimer();
|
||
}
|
||
};
|
||
|
||
const handleRateChange = (rate: number) => {
|
||
if (videoRef.current) {
|
||
videoRef.current.playbackRate = rate;
|
||
setPlaybackRate(rate);
|
||
setShowRateMenu(false);
|
||
resetControlsTimer();
|
||
}
|
||
};
|
||
|
||
const toggleMute = () => {
|
||
if (videoRef.current) {
|
||
videoRef.current.muted = !isMuted;
|
||
setIsMuted(!isMuted);
|
||
}
|
||
};
|
||
|
||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const val = Number(e.target.value);
|
||
setVolume(val);
|
||
if (videoRef.current) {
|
||
videoRef.current.volume = val;
|
||
if (val === 0) {
|
||
setIsMuted(true);
|
||
videoRef.current.muted = true;
|
||
} else {
|
||
setIsMuted(false);
|
||
videoRef.current.muted = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const toggleFullscreen = async () => {
|
||
if (!containerRef.current) return;
|
||
try {
|
||
if (!document.fullscreenElement) {
|
||
await containerRef.current.requestFullscreen();
|
||
setIsFullscreen(true);
|
||
} else {
|
||
await document.exitFullscreen();
|
||
setIsFullscreen(false);
|
||
}
|
||
} catch (err) {
|
||
console.error('Fullscreen toggle failed:', err);
|
||
}
|
||
};
|
||
|
||
const handleShare = async () => {
|
||
const shareUrl = window.location.href;
|
||
if (navigator.share) {
|
||
try {
|
||
await navigator.share({
|
||
title: video?.title || 'ویدئوی راهنمای کنینا',
|
||
text: video?.description || '',
|
||
url: shareUrl
|
||
});
|
||
} catch {
|
||
// Fallback to clipboard
|
||
}
|
||
} else {
|
||
try {
|
||
await navigator.clipboard.writeText(shareUrl);
|
||
setIsCopied(true);
|
||
setTimeout(() => setIsCopied(false), 2500);
|
||
} catch (err) {
|
||
console.error('Copy failed:', err);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleDownload = () => {
|
||
if (!video?.videoUrl) return;
|
||
const a = document.createElement('a');
|
||
a.href = video.videoUrl;
|
||
a.download = `${video.title || 'canina-video'}.mp4`;
|
||
a.target = '_blank';
|
||
a.rel = 'noopener noreferrer';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
};
|
||
|
||
const formatTime = (secs: number) => {
|
||
if (isNaN(secs) || secs < 0) return "00:00";
|
||
const mins = Math.floor(secs / 60);
|
||
const remainingSecs = Math.floor(secs % 60);
|
||
return `${mins.toString().padStart(2, "0")}:${remainingSecs.toString().padStart(2, "0")}`;
|
||
};
|
||
|
||
if (!isOpen || !video) return null;
|
||
|
||
const isIframe = video.videoUrl?.includes("<iframe");
|
||
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||
const bufferedPercent = duration > 0 ? (bufferedEnd / duration) * 100 : 0;
|
||
const volumePercent = isMuted ? 0 : volume * 100;
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
<div className="fixed inset-0 z-[120] flex items-center justify-center p-3 sm:p-6 font-vazir overflow-y-auto" dir="rtl">
|
||
{/* Backdrop (backdrop dismiss disabled to prevent accidental closure) */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/90 backdrop-blur-xl"
|
||
/>
|
||
|
||
{/* Modal Window Wrapper */}
|
||
<div className="relative w-full max-w-5xl flex flex-col gap-3 my-auto z-10">
|
||
<motion.div
|
||
ref={containerRef}
|
||
initial={{ opacity: 0, scale: 0.95, y: 15 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 15 }}
|
||
transition={{ duration: 0.25 }}
|
||
onMouseMove={resetControlsTimer}
|
||
onTouchStart={resetControlsTimer}
|
||
className="relative bg-black w-full aspect-video rounded-3xl sm:rounded-[2.5rem] overflow-hidden shadow-2xl border border-white/10 flex flex-col justify-between select-none"
|
||
>
|
||
{/* Iframe Support (Aparat / Youtube) */}
|
||
{isIframe ? (
|
||
<div className="w-full h-full flex items-center justify-center relative">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="absolute top-4 left-4 z-30 p-2.5 rounded-full bg-black/60 hover:bg-black/90 text-white transition-all cursor-pointer"
|
||
title="بستن"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
<div
|
||
className="w-full h-full [&>iframe]:w-full [&>iframe]:h-full"
|
||
dangerouslySetInnerHTML={{
|
||
__html: typeof window !== 'undefined'
|
||
? DOMPurify.sanitize(video.videoUrl || '', {
|
||
ALLOWED_TAGS: ['iframe'],
|
||
ALLOWED_ATTR: ['src', 'width', 'height', 'frameborder', 'allow', 'allowfullscreen', 'title'],
|
||
})
|
||
: video.videoUrl || '',
|
||
}}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* Native Video Element */}
|
||
<div
|
||
className="absolute inset-0 flex items-center justify-center cursor-pointer"
|
||
onClick={togglePlay}
|
||
>
|
||
<video
|
||
ref={videoRef}
|
||
src={getMediaUrl(video.videoUrl)}
|
||
poster={getMediaUrl(video.thumbnail)}
|
||
autoPlay
|
||
playsInline
|
||
onPlay={() => setIsPlaying(true)}
|
||
onPause={() => setIsPlaying(false)}
|
||
onTimeUpdate={() => {
|
||
if (videoRef.current) {
|
||
setCurrentTime(videoRef.current.currentTime);
|
||
updateBuffer();
|
||
}
|
||
}}
|
||
onProgress={updateBuffer}
|
||
onLoadedMetadata={() => {
|
||
if (videoRef.current) setDuration(videoRef.current.duration);
|
||
setIsBuffering(false);
|
||
updateBuffer();
|
||
}}
|
||
onWaiting={() => setIsBuffering(true)}
|
||
onPlaying={() => setIsBuffering(false)}
|
||
onEnded={() => {
|
||
setIsPlaying(false);
|
||
setShowControls(true);
|
||
}}
|
||
className="w-full h-full object-contain"
|
||
/>
|
||
|
||
{/* Buffering Spinner */}
|
||
{isBuffering && (
|
||
<div className="absolute inset-0 flex items-center justify-center bg-black/30 backdrop-blur-2xs pointer-events-none">
|
||
<Loader2 className="w-12 h-12 text-white animate-spin" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Top Header Overlay */}
|
||
<div
|
||
className={`absolute top-0 left-0 right-0 p-4 sm:p-6 bg-gradient-to-b from-black/80 via-black/40 to-transparent transition-opacity duration-300 z-20 flex items-center justify-between ${showControls ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-3 text-right">
|
||
<div className="w-8 h-8 rounded-full bg-canina-blue/20 text-canina-blue flex items-center justify-center shrink-0">
|
||
<Sparkles className="w-4 h-4" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-white text-sm sm:text-base md:text-lg font-black line-clamp-1">
|
||
{video.title}
|
||
</h3>
|
||
<p className="text-white/60 text-xs font-bold mt-0.5">
|
||
{video.doctor || "کادر علمی کنینا"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
{/* Share button */}
|
||
<button
|
||
type="button"
|
||
onClick={(e) => { e.stopPropagation(); handleShare(); }}
|
||
className="p-2.5 rounded-full bg-white/10 hover:bg-white/20 text-white transition-all cursor-pointer relative"
|
||
title="اشتراکگذاری"
|
||
>
|
||
{isCopied ? <Check className="w-4 h-4 text-emerald-400" /> : <Share2 className="w-4 h-4" />}
|
||
</button>
|
||
|
||
{/* Download button */}
|
||
<button
|
||
type="button"
|
||
onClick={(e) => { e.stopPropagation(); handleDownload(); }}
|
||
className="p-2.5 rounded-full bg-white/10 hover:bg-white/20 text-white transition-all cursor-pointer"
|
||
title="دانلود ویدئو"
|
||
>
|
||
<Download className="w-4 h-4" />
|
||
</button>
|
||
|
||
{/* Close button */}
|
||
<button
|
||
type="button"
|
||
onClick={(e) => { e.stopPropagation(); onClose(); }}
|
||
className="p-2.5 rounded-full bg-white/10 hover:bg-red-500/80 text-white transition-all cursor-pointer"
|
||
title="بستن"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Bottom Custom Controls Bar */}
|
||
<div
|
||
className={`absolute bottom-0 left-0 right-0 p-4 sm:p-6 bg-gradient-to-t from-black/90 via-black/60 to-transparent transition-opacity duration-300 z-20 space-y-3 ${showControls ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||
}`}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
|
||
{/* Progress / Seek Bar with filled progress & buffer tracks */}
|
||
<div className="flex items-center gap-3 dir-ltr" dir="ltr">
|
||
<span className="text-[11px] font-mono font-bold text-white/70 w-10 text-right">
|
||
{formatTime(currentTime)}
|
||
</span>
|
||
<div className="relative flex-1 flex items-center h-4">
|
||
{/* Background & Buffer & Progress Track */}
|
||
<div className="absolute inset-x-0 h-1.5 bg-white/20 rounded-lg overflow-hidden pointer-events-none">
|
||
{/* Buffered bar */}
|
||
<div
|
||
className="h-full bg-white/35 transition-all duration-300"
|
||
style={{ width: `${bufferedPercent}%` }}
|
||
/>
|
||
{/* Played progress bar */}
|
||
<div
|
||
className="h-full bg-canina-blue transition-all -mt-1.5"
|
||
style={{ width: `${progressPercent}%` }}
|
||
/>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min={0}
|
||
max={duration || 100}
|
||
step={0.1}
|
||
value={currentTime}
|
||
onChange={handleSeek}
|
||
className="w-full h-4 opacity-0 absolute inset-0 cursor-pointer z-10"
|
||
/>
|
||
{/* Visual Scrubber Thumb */}
|
||
<div
|
||
className="absolute w-3.5 h-3.5 bg-white rounded-full shadow-md pointer-events-none -translate-x-1/2 transition-transform hover:scale-125"
|
||
style={{ left: `${progressPercent}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-[11px] font-mono font-bold text-white/70 w-10 text-left">
|
||
{formatTime(duration)}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Action Buttons Row */}
|
||
<div className="flex items-center justify-between" dir="ltr">
|
||
{/* Left Controls (Playback & Jumps with - on Left and + on Right in RTL) */}
|
||
<div className="flex items-center gap-2 sm:gap-3" dir="ltr">
|
||
{/* Play/Pause */}
|
||
<button
|
||
type="button"
|
||
onClick={togglePlay}
|
||
className="w-10 h-10 rounded-full bg-canina-blue hover:bg-canina-blue/90 text-white flex items-center justify-center transition-transform hover:scale-105 cursor-pointer shadow-lg shadow-canina-blue/30"
|
||
title={isPlaying ? "توقف" : "پخش"}
|
||
>
|
||
{isPlaying ? <Pause className="w-5 h-5 fill-white" /> : <Play className="w-5 h-5 fill-white ml-0.5" />}
|
||
</button>
|
||
|
||
{/* -10s button */}
|
||
<button
|
||
type="button"
|
||
onClick={() => skipTime(-10)}
|
||
className="px-2.5 py-1.5 text-white/80 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer flex items-center gap-1 text-xs font-mono font-bold border border-white/10"
|
||
title="۱۰ ثانیه عقب"
|
||
>
|
||
<RotateCcw className="w-3.5 h-3.5" />
|
||
<span>-10s</span>
|
||
</button>
|
||
|
||
{/* +10s button */}
|
||
<button
|
||
type="button"
|
||
onClick={() => skipTime(10)}
|
||
className="px-2.5 py-1.5 text-white/80 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer flex items-center gap-1 text-xs font-mono font-bold border border-white/10"
|
||
title="۱۰ ثانیه جلو"
|
||
>
|
||
<RotateCw className="w-3.5 h-3.5" />
|
||
<span>+10s</span>
|
||
</button>
|
||
|
||
{/* Volume Slider: Increases towards the speaker icon */}
|
||
<div className="hidden sm:flex items-center gap-2 pr-2" dir="ltr">
|
||
<div className="relative w-20 flex items-center h-4">
|
||
<div className="absolute inset-x-0 h-1.5 bg-white/20 rounded-lg overflow-hidden pointer-events-none">
|
||
<div
|
||
className="h-full bg-canina-blue transition-all"
|
||
style={{ width: `${volumePercent}%` }}
|
||
/>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min={0}
|
||
max={1}
|
||
step={0.05}
|
||
value={isMuted ? 0 : volume}
|
||
onChange={handleVolumeChange}
|
||
className="w-full h-4 opacity-0 absolute inset-0 cursor-pointer z-10"
|
||
/>
|
||
<div
|
||
className="absolute w-2.5 h-2.5 bg-white rounded-full shadow pointer-events-none -translate-x-1/2"
|
||
style={{ left: `${volumePercent}%` }}
|
||
/>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={toggleMute}
|
||
className="p-1.5 text-white/80 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer"
|
||
title={isMuted ? "صدا وصل" : "بیصدا"}
|
||
>
|
||
{isMuted || volume === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Controls (Speed Menu & Fullscreen) */}
|
||
<div className="flex items-center gap-2">
|
||
{/* Playback Rate Popup Menu with click-outside */}
|
||
<div className="relative" ref={rateMenuRef}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowRateMenu(!showRateMenu)}
|
||
className="px-2.5 py-1 rounded-xl bg-white/10 hover:bg-white/20 text-white text-xs font-mono font-bold transition-all cursor-pointer flex items-center gap-1 border border-white/10"
|
||
title="سرعت پخش"
|
||
>
|
||
<span>{playbackRate}x</span>
|
||
</button>
|
||
|
||
{showRateMenu && (
|
||
<div className="absolute bottom-full left-0 mb-2 bg-neutral-900/95 backdrop-blur-md border border-white/10 rounded-2xl p-1.5 shadow-2xl z-30 flex flex-col gap-1 min-w-[75px]">
|
||
{PLAYBACK_RATES.map((rate) => (
|
||
<button
|
||
key={rate}
|
||
type="button"
|
||
onClick={() => handleRateChange(rate)}
|
||
className={`px-3 py-1.5 text-xs font-mono font-bold rounded-xl text-center transition-colors cursor-pointer ${playbackRate === rate
|
||
? "bg-canina-blue text-white"
|
||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||
}`}
|
||
>
|
||
{rate}x
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Fullscreen Button */}
|
||
<button
|
||
type="button"
|
||
onClick={toggleFullscreen}
|
||
className="p-2 text-white/70 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer"
|
||
title={isFullscreen ? "خروج از تمام صفحه" : "تمام صفحه"}
|
||
>
|
||
{isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* Video Description Box (Rendered outside & below the video player for clean viewing) */}
|
||
{video.description && (
|
||
<div className="bg-neutral-900/90 backdrop-blur-md rounded-2xl p-4 text-white text-xs border border-white/10 shadow-xl space-y-2 text-right">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-black text-canina-blue text-xs">توضیحات و نکات علمی ویدئو:</span>
|
||
{video.description.length > 120 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsExpandedDesc(!isExpandedDesc)}
|
||
className="text-white/60 hover:text-white text-[11px] font-bold inline-flex items-center gap-1 cursor-pointer"
|
||
>
|
||
<span>{isExpandedDesc ? "بستن متن" : "مشاهده کامل متن"}</span>
|
||
{isExpandedDesc ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||
</button>
|
||
)}
|
||
</div>
|
||
<p className={`font-medium text-white/80 leading-relaxed ${isExpandedDesc ? "" : "line-clamp-2"}`}>
|
||
{video.description}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</AnimatePresence>
|
||
);
|
||
}
|