fix(ux): refine hero slider, update shop copy, fix admin product save validation, add soundcloud waveform & fix player controls, improve reviews layout
All checks were successful
Deploy Canina / deploy (push) Successful in 1m39s
All checks were successful
Deploy Canina / deploy (push) Successful in 1m39s
This commit is contained in:
parent
f9dad098ef
commit
f0f50c75cc
@ -308,16 +308,76 @@ export default function Products() {
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Explicit Tab-Aware Client-Side Validation
|
||||
if (!formData.artNo?.trim()) {
|
||||
setActiveTab('general');
|
||||
toast.error('لطفاً کد محصول (Art No) را وارد نمایید');
|
||||
return;
|
||||
}
|
||||
if (!formData.nameFa?.trim()) {
|
||||
setActiveTab('general');
|
||||
toast.error('لطفاً نام فارسی محصول را وارد نمایید');
|
||||
return;
|
||||
}
|
||||
if (!formData.nameEn?.trim()) {
|
||||
setActiveTab('general');
|
||||
toast.error('لطفاً نام انگلیسی محصول را وارد نمایید');
|
||||
return;
|
||||
}
|
||||
if (!formData.categoryId?.trim()) {
|
||||
setActiveTab('general');
|
||||
toast.error('لطفاً دستهبندی محصول را انتخاب نمایید');
|
||||
return;
|
||||
}
|
||||
if (formData.priceValue === undefined || formData.priceValue === null || isNaN(Number(formData.priceValue))) {
|
||||
setActiveTab('pricing');
|
||||
toast.error('لطفاً قیمت فروش محصول را وارد نمایید');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const derivedSlug = formData.slug.trim() || slugify(formData.nameEn);
|
||||
const finalPayload = {
|
||||
...formData,
|
||||
const finalPayload: Record<string, unknown> = {
|
||||
artNo: formData.artNo.trim(),
|
||||
nameFa: formData.nameFa.trim(),
|
||||
nameEn: formData.nameEn.trim(),
|
||||
scientificTagline: formData.scientificTagline.trim() || undefined,
|
||||
description: formData.description.trim() || undefined,
|
||||
shortDescription: formData.shortDescription.trim() || undefined,
|
||||
categoryId: formData.categoryId.trim(),
|
||||
buyPrice: formData.buyPrice ? Number(formData.buyPrice) : 0,
|
||||
wholesalePrice: formData.wholesalePrice ? Number(formData.wholesalePrice) : null,
|
||||
slug: derivedSlug,
|
||||
priceValue: Number(formData.priceValue || 0),
|
||||
wholesalePrice: formData.wholesalePrice ? Number(formData.wholesalePrice) : undefined,
|
||||
priceValueMarginPercent: formData.priceValueMarginPercent !== '' ? Number(formData.priceValueMarginPercent) : undefined,
|
||||
wholesaleMarginPercent: formData.wholesaleMarginPercent !== '' ? Number(formData.wholesaleMarginPercent) : undefined,
|
||||
priceDisplay: formData.priceDisplay || `${Number(formData.priceValue || 0).toLocaleString('fa-IR')} تومان`,
|
||||
unit: formData.unit.trim() || undefined,
|
||||
packageSize: Number(formData.packageSize) || 0,
|
||||
dosageLogic: formData.dosageLogic.trim() || undefined,
|
||||
suitableFor: formData.suitableFor || 'سگ و گربه',
|
||||
imageUrl: formData.imageUrl.trim() || undefined,
|
||||
images: formData.images || [],
|
||||
podcastUrl: formData.podcastUrl.trim() || undefined,
|
||||
podcastTitle: formData.podcastTitle.trim() || undefined,
|
||||
podcastDescription: formData.podcastDescription.trim() || undefined,
|
||||
podcastCover: formData.podcastCover.trim() || undefined,
|
||||
videoUrl: formData.videoUrl.trim() || undefined,
|
||||
videoTitle: formData.videoTitle.trim() || undefined,
|
||||
videoDescription: formData.videoDescription.trim() || undefined,
|
||||
videoCover: formData.videoCover.trim() || undefined,
|
||||
pdfUrl: formData.pdfUrl.trim() || undefined,
|
||||
pdfTitle: formData.pdfTitle.trim() || undefined,
|
||||
pdfDescription: formData.pdfDescription.trim() || undefined,
|
||||
pdfCover: formData.pdfCover.trim() || undefined,
|
||||
metaTitle: formData.metaTitle.trim() || `${formData.nameFa || 'نام محصول'} | خرید و قیمت مکمل کنینا`,
|
||||
metaDescription: formData.metaDescription.trim() || formData.shortDescription || 'خرید آنلاین مکمل اصل کنینا آلمان با بالاترین کیفیت بالینی...',
|
||||
canonicalUrl: formData.canonicalUrl.trim() || `/shop/${derivedSlug}`
|
||||
keywords: formData.keywords.trim() || undefined,
|
||||
canonicalUrl: formData.canonicalUrl.trim() || `/shop/${derivedSlug}`,
|
||||
slug: derivedSlug,
|
||||
symptoms: formData.symptoms || [],
|
||||
isPreorder: Boolean(formData.isPreorder),
|
||||
preorderDeposit: formData.preorderDeposit ? String(formData.preorderDeposit) : undefined
|
||||
};
|
||||
|
||||
if (editingProduct) {
|
||||
@ -338,9 +398,12 @@ export default function Products() {
|
||||
setIsModalOpen(false);
|
||||
setImageErrors({});
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Save failed', error);
|
||||
toast.error('خطا در ذخیره محصول');
|
||||
const serverMsg = error.response?.data?.message || error.message;
|
||||
if (serverMsg && !error._toastShown) {
|
||||
toast.error(`خطا در ذخیره محصول: ${serverMsg}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -365,16 +365,16 @@ export default function Header({
|
||||
onMouseLeave={() => setActiveDropdown(null)}
|
||||
>
|
||||
<button className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-medical-gray-700 hover:bg-medical-gray-100 hover:text-canina-blue transition-all">
|
||||
<span>محصولات و کاتالوگ</span>
|
||||
<span>فروشگاه و محصولات</span>
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
{activeDropdown === 'products' && (
|
||||
<div className="absolute top-full right-0 w-52 bg-white border border-medical-gray-200 shadow-xl rounded-xl p-2 z-50 space-y-1 text-xs">
|
||||
<div className="absolute top-full right-0 w-56 bg-white border border-medical-gray-200 shadow-xl rounded-xl p-2 z-50 space-y-1 text-xs">
|
||||
<Link href="/shop" className="block px-3 py-2 hover:bg-canina-blue/5 hover:text-canina-blue rounded-lg font-bold">
|
||||
کاتالوگ جامع محصولات
|
||||
فروشگاه تخصصی محصولات کنینا
|
||||
</Link>
|
||||
<Link href="/catalog" className="block px-3 py-2 hover:bg-canina-blue/5 hover:text-canina-blue rounded-lg font-bold">
|
||||
کاتالوگ آنلاین و دوز مصرفی
|
||||
کاتالوگ دیجیتال و راهنمای بالینی
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -70,7 +70,7 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
const subtitle = (mounted && isInitialized ? getText('hero_desc', '') : '') || defaultSubtitle;
|
||||
const titleParts = title.split('\n');
|
||||
|
||||
const activeImageUrl = activeBanner?.imageUrl || (mounted && isInitialized ? getText('hero_image_url', '') : '') || (banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "/assets/images/hero-section-image.png");
|
||||
const activeImageUrl = activeBanner?.imageUrl || (mounted && isInitialized ? getText('hero_image_url', '') : '') || (banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "");
|
||||
|
||||
const handleNextSlide = () => {
|
||||
if (totalSlides > 1) {
|
||||
@ -193,18 +193,20 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
onTouchEnd={onTouchEnd}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.img
|
||||
key={activeImageUrl}
|
||||
src={activeImageUrl}
|
||||
alt={activeBanner?.title || "Canina Pharma Germany"}
|
||||
initial={{ opacity: 0, scale: 1.05 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
{activeImageUrl && (
|
||||
<motion.img
|
||||
key={activeImageUrl}
|
||||
src={activeImageUrl}
|
||||
alt={activeBanner?.title || "Canina Pharma Germany"}
|
||||
initial={{ opacity: 0, scale: 1.05 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Gradient overlay and slide caption */}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import {
|
||||
Play,
|
||||
@ -32,8 +32,13 @@ interface PodcastPlayerModalProps {
|
||||
|
||||
const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5];
|
||||
|
||||
// Generate dynamic waveform bars for Soundcloud-like UI
|
||||
const WAVEFORM_BAR_COUNT = 48;
|
||||
|
||||
export default function PodcastPlayerModal({ isOpen, onClose, podcast }: PodcastPlayerModalProps) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const waveformRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
@ -44,6 +49,16 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
const [isBuffering, setIsBuffering] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
// Deterministic heights for waveform visualization
|
||||
const waveformHeights = useMemo(() => {
|
||||
const seed = (podcast?.title || "canina").split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return Array.from({ length: WAVEFORM_BAR_COUNT }).map((_, i) => {
|
||||
const pseudo = Math.sin((i + 1) * 0.45 + seed) * 0.5 + 0.5;
|
||||
const heightPercent = Math.max(20, Math.min(95, Math.floor(pseudo * 80 + 15)));
|
||||
return heightPercent;
|
||||
});
|
||||
}, [podcast?.title]);
|
||||
|
||||
// Reset states on open/close
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@ -67,14 +82,22 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const time = Number(e.target.value);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = time;
|
||||
setCurrentTime(time);
|
||||
const seekToPercent = (percent: number) => {
|
||||
if (audioRef.current && duration > 0) {
|
||||
const newTime = (percent / 100) * duration;
|
||||
audioRef.current.currentTime = Math.max(0, Math.min(duration, newTime));
|
||||
setCurrentTime(newTime);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWaveformClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!waveformRef.current || duration <= 0) return;
|
||||
const rect = waveformRef.current.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const percent = Math.max(0, Math.min(100, (clickX / rect.width) * 100));
|
||||
seekToPercent(percent);
|
||||
};
|
||||
|
||||
const skipTime = (seconds: number) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = Math.max(0, Math.min(audioRef.current.duration || 0, audioRef.current.currentTime + seconds));
|
||||
@ -116,7 +139,7 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: podcast?.title || 'پادکست و بررسی تخصصی کنینا',
|
||||
title: podcast?.title || 'پادکست و بررسی بالینی مکمل کنینا',
|
||||
text: podcast?.description || '',
|
||||
url: shareUrl
|
||||
});
|
||||
@ -156,6 +179,8 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
if (!isOpen || !podcast) return null;
|
||||
|
||||
const displayCover = podcast.cover || podcast.fallbackCover || '/images/default-podcast.png';
|
||||
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||
const volumePercent = isMuted ? 0 : volume * 100;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@ -178,10 +203,10 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
className="relative bg-gradient-to-b from-neutral-900 via-neutral-900 to-black w-full max-w-lg rounded-[2.5rem] overflow-hidden shadow-2xl border border-white/15 p-6 sm:p-8 flex flex-col items-center select-none text-white z-10"
|
||||
>
|
||||
{/* Top Bar (Header + Close) */}
|
||||
<div className="w-full flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2.5 text-xs font-black text-canina-blue bg-canina-blue/10 px-3.5 py-1.5 rounded-full border border-canina-blue/20">
|
||||
<div className="w-full flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2.5 text-xs font-black text-purple-400 bg-purple-500/10 px-3.5 py-1.5 rounded-full border border-purple-500/20">
|
||||
<Headphones className="w-4 h-4 animate-pulse" />
|
||||
<span>پادکست و بررسی صوتی کنینا</span>
|
||||
<span>پادکست و تحلیل صوتی بالینی</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
@ -235,7 +260,7 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
|
||||
{/* Cover Art Artwork */}
|
||||
<div className="relative group my-2">
|
||||
<div className="w-48 h-48 sm:w-56 sm:h-56 rounded-3xl overflow-hidden shadow-2xl border-2 border-white/20 relative bg-neutral-800 flex items-center justify-center">
|
||||
<div className="w-40 h-40 sm:w-48 sm:h-48 rounded-3xl overflow-hidden shadow-2xl border-2 border-white/20 relative bg-neutral-800 flex items-center justify-center">
|
||||
<img
|
||||
src={displayCover}
|
||||
alt={podcast.title || "کاور پادکست"}
|
||||
@ -255,43 +280,60 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
</div>
|
||||
|
||||
{/* Title and Metadata */}
|
||||
<div className="w-full text-center mt-5 mb-4 space-y-1.5">
|
||||
<h3 className="text-lg sm:text-xl font-black text-white line-clamp-1">
|
||||
<div className="w-full text-center mt-3 mb-3 space-y-1">
|
||||
<h3 className="text-base sm:text-lg font-black text-white line-clamp-1">
|
||||
{podcast.title || "بررسی بالینی و نحوه مصرف مکمل"}
|
||||
</h3>
|
||||
{podcast.productName && (
|
||||
<p className="text-canina-blue text-xs font-bold flex items-center justify-center gap-1">
|
||||
<p className="text-purple-400 text-xs font-bold flex items-center justify-center gap-1">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>محصول: {podcast.productName}</span>
|
||||
</p>
|
||||
)}
|
||||
{podcast.description && (
|
||||
<p className="text-neutral-300 text-xs line-clamp-2 px-2 pt-1 font-medium leading-relaxed">
|
||||
<p className="text-neutral-300 text-xs line-clamp-2 px-2 pt-0.5 font-medium leading-relaxed">
|
||||
{podcast.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Seeker / Progress Bar */}
|
||||
<div className="w-full space-y-1.5 mt-2 mb-4">
|
||||
<div className="flex items-center gap-3 dir-ltr" dir="ltr">
|
||||
<span className="text-[11px] font-mono font-bold text-white/60 w-10 text-right">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<div className="relative flex-1 flex items-center">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={duration || 100}
|
||||
step={0.1}
|
||||
value={currentTime}
|
||||
onChange={handleSeek}
|
||||
className="w-full h-2 bg-white/20 rounded-lg appearance-none cursor-pointer accent-canina-blue hover:h-2.5 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[11px] font-mono font-bold text-white/60 w-10 text-left">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
{/* Soundcloud-like Waveform Visualizer & Seeker */}
|
||||
<div className="w-full space-y-2 mt-2 mb-3">
|
||||
<div
|
||||
ref={waveformRef}
|
||||
onClick={handleWaveformClick}
|
||||
className="w-full h-12 flex items-center justify-between gap-[3px] px-2 py-1 bg-white/5 hover:bg-white/10 rounded-2xl cursor-pointer transition-all relative overflow-hidden group/wave border border-white/5"
|
||||
title="برای رفتن به بخش مورد نظر کلیک کنید"
|
||||
dir="ltr"
|
||||
>
|
||||
{waveformHeights.map((h, i) => {
|
||||
const barPercent = (i / (WAVEFORM_BAR_COUNT - 1)) * 100;
|
||||
const isPassed = barPercent <= progressPercent;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 flex items-center justify-center h-full"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-full transition-all duration-150 ${
|
||||
isPassed
|
||||
? "bg-gradient-to-t from-purple-500 to-canina-blue shadow-xs"
|
||||
: "bg-white/20 group-hover/wave:bg-white/30"
|
||||
} ${isPlaying && isPassed ? "opacity-100" : "opacity-85"}`}
|
||||
style={{
|
||||
height: `${h}%`,
|
||||
transform: isPlaying && isPassed ? "scaleY(1.05)" : "scaleY(1)"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Time Stamp Row */}
|
||||
<div className="flex items-center justify-between text-[11px] font-mono font-bold text-white/60 px-1 dir-ltr" dir="ltr">
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
<span>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -302,7 +344,7 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRateMenu(!showRateMenu)}
|
||||
className="px-3 py-1.5 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"
|
||||
className="px-2.5 py-1.5 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>
|
||||
@ -317,7 +359,7 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
onClick={() => handleRateChange(rate)}
|
||||
className={`px-3 py-1 text-xs font-mono font-bold rounded-xl text-center transition-colors cursor-pointer ${
|
||||
playbackRate === rate
|
||||
? "bg-canina-blue text-white"
|
||||
? "bg-purple-600 text-white"
|
||||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
@ -328,60 +370,72 @@ export default function PodcastPlayerModal({ isOpen, onClose, podcast }: Podcast
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Central Controls: -15s, Play/Pause, +15s */}
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
{/* -15s */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => skipTime(-15)}
|
||||
className="p-2.5 text-white/80 hover:text-white rounded-full hover:bg-white/10 transition-colors cursor-pointer flex items-center gap-0.5 text-xs font-mono font-bold"
|
||||
title="۱۵ ثانیه عقب"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
<span className="text-[10px]">15s</span>
|
||||
</button>
|
||||
|
||||
{/* Central Controls: -15s on Left, Play/Pause in Middle, +15s on Right in RTL */}
|
||||
<div className="flex items-center gap-2 sm:gap-3" dir="rtl">
|
||||
{/* Play / Pause button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePlay}
|
||||
className="w-14 h-14 rounded-full bg-canina-blue hover:bg-canina-blue/90 text-white flex items-center justify-center transition-transform hover:scale-105 active:scale-95 cursor-pointer shadow-xl shadow-canina-blue/40 border border-white/20"
|
||||
className="w-12 h-12 rounded-full bg-purple-600 hover:bg-purple-500 text-white flex items-center justify-center transition-transform hover:scale-105 active:scale-95 cursor-pointer shadow-xl shadow-purple-600/40 border border-white/20"
|
||||
title={isPlaying ? "توقف" : "پخش"}
|
||||
>
|
||||
{isPlaying ? <Pause className="w-6 h-6 fill-white" /> : <Play className="w-6 h-6 fill-white ml-0.5" />}
|
||||
{isPlaying ? <Pause className="w-5 h-5 fill-white" /> : <Play className="w-5 h-5 fill-white ml-0.5" />}
|
||||
</button>
|
||||
|
||||
{/* +15s */}
|
||||
{/* -15s button (left) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => skipTime(-15)}
|
||||
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>-15s</span>
|
||||
</button>
|
||||
|
||||
{/* +15s button (right) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => skipTime(15)}
|
||||
className="p-2.5 text-white/80 hover:text-white rounded-full hover:bg-white/10 transition-colors cursor-pointer flex items-center gap-0.5 text-xs font-mono font-bold"
|
||||
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-5 h-5" />
|
||||
<span className="text-[10px]">15s</span>
|
||||
<RotateCw className="w-3.5 h-3.5" />
|
||||
<span>+15s</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Volume Control */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Volume Control: Increases towards the speaker icon */}
|
||||
<div className="flex items-center gap-2" dir="ltr">
|
||||
<div className="relative w-16 hidden sm: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-purple-500 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-2 text-white/80 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer"
|
||||
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>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-14 h-1.5 bg-white/20 rounded-lg appearance-none cursor-pointer accent-canina-blue hidden sm:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@ -692,140 +692,134 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Video Player Card */}
|
||||
<div className="space-y-6">
|
||||
{/* Video Player Row */}
|
||||
{product.videoUrl && (
|
||||
<div className="bg-gradient-to-br from-neutral-900 via-neutral-900 to-black rounded-3xl overflow-hidden shadow-xl border border-white/10 p-6 flex flex-col justify-between text-white space-y-4 group">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-black text-blue-400 bg-blue-500/10 px-3 py-1 rounded-full border border-blue-500/20 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
|
||||
ویدیو راهنما
|
||||
</span>
|
||||
<span className="text-[11px] text-white/50 font-mono">Video Guide</span>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail / Cover Box */}
|
||||
<div
|
||||
onClick={() => setIsVideoModalOpen(true)}
|
||||
className="relative aspect-video rounded-2xl overflow-hidden cursor-pointer bg-neutral-800 border border-white/10 group-hover:border-canina-blue/50 transition-all flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={product.videoCover || product.image}
|
||||
alt={product.videoTitle || product.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = product.image;
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||
<div className="w-14 h-14 rounded-full bg-canina-blue text-white flex items-center justify-center shadow-xl shadow-canina-blue/50 group-hover:scale-110 transition-transform">
|
||||
<Play className="w-6 h-6 fill-white ml-0.5" />
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-neutral-900 via-neutral-900 to-black rounded-3xl overflow-hidden shadow-xl border border-white/10 p-6 flex flex-col md:flex-row items-center gap-6 text-white group">
|
||||
{/* Thumbnail / Cover Box */}
|
||||
<div
|
||||
onClick={() => setIsVideoModalOpen(true)}
|
||||
className="relative w-full md:w-80 aspect-video rounded-2xl overflow-hidden cursor-pointer bg-neutral-800 border border-white/10 group-hover:border-canina-blue/50 transition-all flex items-center justify-center shrink-0"
|
||||
>
|
||||
<img
|
||||
src={product.videoCover || product.image}
|
||||
alt={product.videoTitle || product.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = product.image;
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||
<div className="w-12 h-12 rounded-full bg-canina-blue text-white flex items-center justify-center shadow-xl shadow-canina-blue/50 group-hover:scale-110 transition-transform">
|
||||
<Play className="w-5 h-5 fill-white ml-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-base font-black text-white line-clamp-1">
|
||||
{product.videoTitle || "ویدیو معرفی و راهنمای مصرف محصول"}
|
||||
</h4>
|
||||
<p className="text-xs text-white/70 font-medium line-clamp-2 mt-1 leading-relaxed">
|
||||
{product.videoDescription || "مشاهده راهنمای کامل، دوز مصرفی و آموزش خوراندن مکمل توسط کادر تخصصی کنینا"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsVideoModalOpen(true)}
|
||||
className="w-full py-3 px-4 bg-white/10 hover:bg-canina-blue text-white rounded-2xl font-black text-xs transition-all flex items-center justify-center gap-2 cursor-pointer border border-white/10"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-white" />
|
||||
<span>پخش ویدیو در پلیر اختصاصی</span>
|
||||
</button>
|
||||
<div className="flex-1 text-center md:text-right space-y-2">
|
||||
<div className="flex items-center justify-center md:justify-start gap-2">
|
||||
<span className="text-xs font-black text-blue-400 bg-blue-500/10 px-3 py-1 rounded-full border border-blue-500/20 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
|
||||
ویدیو راهنما و معرفی
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-lg font-black text-white">
|
||||
{product.videoTitle || "ویدیو راهنمای مصرف و معرفی مکمل"}
|
||||
</h4>
|
||||
<p className="text-xs text-white/70 font-medium leading-relaxed">
|
||||
{product.videoDescription || "مشاهده ویدیوی آموزشی، دوزبندی دقیق و نحوه مصرف مکمل توسط کادر علمی کنینا"}
|
||||
</p>
|
||||
<div className="pt-2 flex justify-center md:justify-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsVideoModalOpen(true)}
|
||||
className="py-2.5 px-6 bg-canina-blue hover:bg-blue-600 text-white rounded-xl font-bold text-xs transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-canina-blue/20"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-white" />
|
||||
<span>پخش ویدیو</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Podcast Player Card */}
|
||||
{/* Podcast Player Row */}
|
||||
{product.podcastUrl && (
|
||||
<div className="bg-gradient-to-br from-purple-950/40 via-neutral-900 to-black rounded-3xl overflow-hidden shadow-xl border border-purple-500/20 p-6 flex flex-col justify-between text-white space-y-4 group">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-black text-purple-400 bg-purple-500/10 px-3 py-1 rounded-full border border-purple-500/20 flex items-center gap-1.5">
|
||||
🎧 پادکست اختصاصی
|
||||
</span>
|
||||
<span className="text-[11px] text-white/50 font-mono">Audio Analysis</span>
|
||||
</div>
|
||||
|
||||
{/* Podcast Cover / Artwork Box */}
|
||||
<div
|
||||
onClick={() => setIsPodcastModalOpen(true)}
|
||||
className="relative aspect-video rounded-2xl overflow-hidden cursor-pointer bg-neutral-800 border border-white/10 group-hover:border-purple-500/50 transition-all flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
src={product.podcastCover || product.image}
|
||||
alt={product.podcastTitle || product.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = product.image;
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||
<div className="w-14 h-14 rounded-full bg-purple-600 text-white flex items-center justify-center shadow-xl shadow-purple-600/50 group-hover:scale-110 transition-transform">
|
||||
<Play className="w-6 h-6 fill-white ml-0.5" />
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-purple-950/40 via-neutral-900 to-black rounded-3xl overflow-hidden shadow-xl border border-purple-500/20 p-6 flex flex-col md:flex-row items-center gap-6 text-white group">
|
||||
{/* Podcast Cover / Artwork Box */}
|
||||
<div
|
||||
onClick={() => setIsPodcastModalOpen(true)}
|
||||
className="relative w-full md:w-80 aspect-video rounded-2xl overflow-hidden cursor-pointer bg-neutral-800 border border-white/10 group-hover:border-purple-500/50 transition-all flex items-center justify-center shrink-0"
|
||||
>
|
||||
<img
|
||||
src={product.podcastCover || product.image}
|
||||
alt={product.podcastTitle || product.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = product.image;
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||
<div className="w-12 h-12 rounded-full bg-purple-600 text-white flex items-center justify-center shadow-xl shadow-purple-600/50 group-hover:scale-110 transition-transform">
|
||||
<Play className="w-5 h-5 fill-white ml-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-base font-black text-white line-clamp-1">
|
||||
{product.podcastTitle || "پادکست و بررسی صوتی بالینی مکمل"}
|
||||
</h4>
|
||||
<p className="text-xs text-white/70 font-medium line-clamp-2 mt-1 leading-relaxed">
|
||||
{product.podcastDescription || "توضیحات صوتی دامپزشک و کادر علمی درباره اثربخشی، مکانیسم اثر و نحوه مصرف دقیق"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPodcastModalOpen(true)}
|
||||
className="w-full py-3 px-4 bg-white/10 hover:bg-purple-600 text-white rounded-2xl font-black text-xs transition-all flex items-center justify-center gap-2 cursor-pointer border border-white/10"
|
||||
<div className="flex-1 text-center md:text-right space-y-2">
|
||||
<div className="flex items-center justify-center md:justify-start gap-2">
|
||||
<span className="text-xs font-black text-purple-400 bg-purple-500/10 px-3 py-1 rounded-full border border-purple-500/20">
|
||||
🎧 پادکست و بررسی صوتی بالینی
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-lg font-black text-white">
|
||||
{product.podcastTitle || "پادکست و بررسی صوتی بالینی مکمل"}
|
||||
</h4>
|
||||
<p className="text-xs text-white/70 font-medium leading-relaxed">
|
||||
{product.podcastDescription || "توضیحات صوتی دامپزشک و کادر علمی درباره اثربخشی، مکانیسم اثر و نحوه مصرف دقیق"}
|
||||
</p>
|
||||
<div className="pt-2 flex justify-center md:justify-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPodcastModalOpen(true)}
|
||||
className="py-2.5 px-6 bg-purple-600 hover:bg-purple-700 text-white rounded-xl font-bold text-xs transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-purple-600/20"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-white" />
|
||||
<span>شنیدن پادکست</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PDF Catalog Download Row */}
|
||||
{product.pdfUrl && (
|
||||
<div className="bg-medical-gray-50 border border-medical-gray-200 rounded-3xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4 shadow-xs">
|
||||
<div className="flex items-center gap-4 text-center sm:text-right">
|
||||
<div className="w-14 h-14 rounded-2xl bg-red-100 text-red-600 flex items-center justify-center font-black text-sm shadow-sm shrink-0">
|
||||
PDF
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-base font-black text-gray-900">
|
||||
{product.pdfTitle || "بروشور و کاتالوگ علمی Canina آلمان"}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 font-medium mt-0.5">
|
||||
{product.pdfDescription || "شامل مقالات رفرنس، جدول ترکیبات دقیق و سرتیفیکیت رسمی"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={product.pdfUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="bg-canina-blue hover:bg-blue-700 text-white px-6 py-3.5 rounded-2xl font-black text-xs transition-all shadow-md shadow-canina-blue/20 flex items-center gap-2 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-white" />
|
||||
<span>شنیدن پادکست در پلیر اختصاصی</span>
|
||||
</button>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>دانلود مستقیم فایل PDF</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PDF Catalog Download Box */}
|
||||
{product.pdfUrl && (
|
||||
<div className="bg-medical-gray-50 border border-medical-gray-200 rounded-3xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4 shadow-xs">
|
||||
<div className="flex items-center gap-4 text-center sm:text-right">
|
||||
<div className="w-14 h-14 rounded-2xl bg-red-100 text-red-600 flex items-center justify-center font-black text-sm shadow-sm shrink-0">
|
||||
PDF
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-base font-black text-gray-900">
|
||||
{product.pdfTitle || "بروشور و کاتالوگ علمی Canina آلمان"}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 font-medium mt-0.5">
|
||||
{product.pdfDescription || "شامل مقالات رفرنس، جدول ترکیبات دقیق و سرتیفیکیت رسمی"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={product.pdfUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="bg-canina-blue hover:bg-blue-700 text-white px-6 py-3.5 rounded-2xl font-black text-xs transition-all shadow-md shadow-canina-blue/20 flex items-center gap-2 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>دانلود مستقیم فایل PDF</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@ -102,10 +102,10 @@ export default function ProductReviews({ productId, productName }: ProductReview
|
||||
return (
|
||||
<section className="bg-white border border-medical-gray-200 rounded-[2.5rem] p-6 sm:p-10 shadow-md space-y-8 font-vazir text-right" dir="rtl">
|
||||
{/* Header & Rating Summary */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 pb-6 border-b border-medical-gray-100">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 pb-6 border-b border-medical-gray-100">
|
||||
<div className="space-y-1 text-right">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-canina-gold/10 text-canina-gold rounded-2xl flex items-center justify-center shadow-xs">
|
||||
<div className="w-12 h-12 bg-canina-gold/10 text-canina-gold rounded-2xl flex items-center justify-center shadow-xs shrink-0">
|
||||
<Star className="w-6 h-6 fill-canina-gold" />
|
||||
</div>
|
||||
<div>
|
||||
@ -120,13 +120,13 @@ export default function ProductReviews({ productId, productName }: ProductReview
|
||||
</div>
|
||||
|
||||
{/* Rating Score Badge & Add Review Button */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-3 px-5 py-3 bg-medical-gray-50 border border-medical-gray-100 rounded-2xl">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex flex-wrap items-center gap-3 justify-start sm:justify-end">
|
||||
<div className="flex items-center gap-2.5 px-4 py-2.5 bg-medical-gray-50 border border-medical-gray-100 rounded-2xl">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${
|
||||
className={`w-3.5 h-3.5 ${
|
||||
i < Math.round(stats.averageRating)
|
||||
? "fill-amber-400 text-amber-400"
|
||||
: "text-medical-gray-200"
|
||||
@ -134,17 +134,18 @@ export default function ProductReviews({ productId, productName }: ProductReview
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm font-black text-medical-gray-900">
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
{toPersian(stats.averageRating)} از ۵
|
||||
</div>
|
||||
<span className="text-xs text-medical-gray-400 font-bold border-r border-medical-gray-200 pr-2">
|
||||
<span className="text-[11px] text-medical-gray-400 font-bold border-r border-medical-gray-200 pr-2">
|
||||
({toPersian(stats.totalReviews)} دیدگاه)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormOpen(!formOpen)}
|
||||
className="px-5 py-3 bg-canina-blue hover:bg-canina-dark text-white rounded-2xl text-xs font-black transition-all shadow-md shadow-canina-blue/20 flex items-center gap-2"
|
||||
className="px-5 py-2.5 bg-medical-gray-900 hover:bg-canina-blue text-white rounded-2xl text-xs font-black transition-all shadow-md shadow-black/10 flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>{formOpen ? "بستن فرم" : "ثبت دیدگاه جدید"}</span>
|
||||
|
||||
@ -173,7 +173,7 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: video?.title || 'ویدئوی آموزشی کنینا',
|
||||
title: video?.title || 'ویدئوی راهنمای کنینا',
|
||||
text: video?.description || '',
|
||||
url: shareUrl
|
||||
});
|
||||
@ -213,6 +213,8 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
if (!isOpen || !video) return null;
|
||||
|
||||
const isIframe = video.videoUrl?.includes("<iframe");
|
||||
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||
const volumePercent = isMuted ? 0 : volume * 100;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@ -370,12 +372,19 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress / Seek Bar */}
|
||||
{/* Progress / Seek Bar with filled progress track */}
|
||||
<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">
|
||||
<div className="relative flex-1 flex items-center h-4">
|
||||
{/* Background Track */}
|
||||
<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: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
@ -383,7 +392,12 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
step={0.1}
|
||||
value={currentTime}
|
||||
onChange={handleSeek}
|
||||
className="w-full h-1.5 bg-white/20 rounded-lg appearance-none cursor-pointer accent-canina-blue hover:h-2 transition-all"
|
||||
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">
|
||||
@ -393,8 +407,8 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
|
||||
{/* Action Buttons Row */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left Controls (Playback & Jumps) */}
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
{/* Left Controls (Playback & Jumps with - on Left and + on Right in RTL) */}
|
||||
<div className="flex items-center gap-2 sm:gap-3" dir="rtl">
|
||||
{/* Play/Pause */}
|
||||
<button
|
||||
type="button"
|
||||
@ -405,47 +419,59 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
{isPlaying ? <Pause className="w-5 h-5 fill-white" /> : <Play className="w-5 h-5 fill-white ml-0.5" />}
|
||||
</button>
|
||||
|
||||
{/* -10s */}
|
||||
{/* -10s button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => skipTime(-10)}
|
||||
className="p-2 text-white/70 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer flex items-center gap-1 text-xs font-mono font-bold"
|
||||
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-4 h-4" />
|
||||
<span className="text-[10px]">10s</span>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
<span>-10s</span>
|
||||
</button>
|
||||
|
||||
{/* +10s */}
|
||||
{/* +10s button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => skipTime(10)}
|
||||
className="p-2 text-white/70 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer flex items-center gap-1 text-xs font-mono font-bold"
|
||||
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-4 h-4" />
|
||||
<span className="text-[10px]">10s</span>
|
||||
<RotateCw className="w-3.5 h-3.5" />
|
||||
<span>+10s</span>
|
||||
</button>
|
||||
|
||||
{/* Volume slider */}
|
||||
<div className="hidden sm:flex items-center gap-2 group/vol">
|
||||
{/* 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-2 text-white/70 hover:text-white rounded-lg hover:bg-white/10 transition-colors cursor-pointer"
|
||||
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>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer accent-canina-blue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -456,7 +482,7 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP
|
||||
<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"
|
||||
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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user