taghirsado / components /ModelGrid.tsx
Opera10's picture
Upload 10 files
a8df197 verified
Raw
History Blame Contribute Delete
10.2 kB
import React, { useState, useRef, useEffect } from 'react';
import { MODELS } from '../constants';
import { Model } from '../types';
interface ModelGridProps {
customModels: Model[];
onSelectModel: (model: Model) => void;
onOpenCustom: () => void;
onAddNewModel: () => void;
onEditModel: (model: Model) => void;
}
const ModelGrid: React.FC<ModelGridProps> = ({
customModels,
onSelectModel,
onOpenCustom,
onAddNewModel,
onEditModel
}) => {
const [activeTab, setActiveTab] = useState('all');
const [playingModelId, setPlayingModelId] = useState<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const categories: { key: string; title: string; icon: string }[] = [
{ key: 'singers', title: 'خوانندگان', icon: 'fa-music' },
{ key: 'dubbers', title: 'دوبلورها', icon: 'fa-microphone-alt' },
{ key: 'cartoons', title: 'کارتون', icon: 'fa-child' },
{ key: 'famous', title: 'مشاهیر', icon: 'fa-star' },
{ key: 'alpha', title: 'گویندگان آلفا', icon: 'fa-user-tie' },
];
useEffect(() => {
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
};
}, []);
const togglePreview = (e: React.MouseEvent, model: Model) => {
e.stopPropagation();
if (playingModelId === model.id) {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setPlayingModelId(null);
return;
}
if (audioRef.current) audioRef.current.pause();
let audioSrc = model.sampleAudio;
// If no sample, and it's custom, maybe use the ref file if it's a blob?
// Actually standard models have sampleAudio, custom ones might use their ref as sample
if (!audioSrc && model.isCustom && typeof model.refFile !== 'string') {
audioSrc = URL.createObjectURL(model.refFile);
}
if (audioSrc) {
const audio = new Audio(audioSrc);
audioRef.current = audio;
audio.onerror = () => setPlayingModelId(null);
audio.play().catch(() => setPlayingModelId(null));
setPlayingModelId(model.id);
audio.onended = () => {
setPlayingModelId(null);
audioRef.current = null;
};
}
};
const renderModelCard = (model: Model) => {
// Handling image URL for custom models (Blobs)
const imageUrl = (model.isCustom && typeof model.image !== 'string')
? URL.createObjectURL(model.image as any)
: model.image;
return (
<div
key={model.id}
onClick={() => onSelectModel(model)}
className="group relative cursor-pointer rounded-2xl overflow-hidden aspect-square shadow-sm hover:shadow-xl transition-all duration-300 active:scale-95 bg-gray-100"
>
<img
src={imageUrl}
alt={model.name}
loading="lazy"
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-80"></div>
{/* Play Button */}
<button
onClick={(e) => togglePreview(e, model)}
className={`absolute top-2 left-2 w-8 h-8 rounded-full flex items-center justify-center backdrop-blur-md shadow-sm transition-all duration-300 z-20
${playingModelId === model.id
? 'bg-red-500/90 text-white scale-110 shadow-red-500/50'
: 'bg-white/30 hover:bg-white/50 text-white hover:scale-105'
}`}
>
{playingModelId === model.id ? (
<div className="flex items-center justify-center h-full w-full relative">
<i className="fas fa-stop text-xs relative z-10"></i>
<span className="absolute inset-0 rounded-full bg-red-400 animate-ping opacity-75"></span>
</div>
) : (
<i className="fas fa-play text-xs ml-0.5"></i>
)}
</button>
{/* Edit Button for Custom Models */}
{model.isCustom && (
<button
onClick={(e) => { e.stopPropagation(); onEditModel(model); }}
className="absolute top-2 right-2 w-8 h-8 rounded-full bg-amber-500/90 text-white flex items-center justify-center shadow-lg hover:scale-110 transition-all z-20"
>
<i className="fas fa-edit text-xs"></i>
</button>
)}
<div className="absolute bottom-0 left-0 right-0 p-3">
<span className="block text-white text-sm font-bold truncate">
{model.name}
</span>
<span className="text-[10px] text-gray-300 flex items-center gap-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform translate-y-2 group-hover:translate-y-0">
<i className="fas fa-wave-square"></i> انتخاب مدل
</span>
</div>
</div>
);
};
return (
<div className="pb-24 animate-[fadeIn_0.5s_ease-out]">
{/* Banner / CTA */}
<div
onClick={onOpenCustom}
className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-gray-900 to-gray-800 p-6 mb-6 shadow-xl shadow-gray-900/20 cursor-pointer group"
>
<div className="absolute top-0 left-0 w-full h-full opacity-10 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')]"></div>
<div className="relative z-10 flex items-center justify-between">
<div>
<h3 className="text-white font-bold text-lg mb-1">هنوز مدل دلخواهت رو پیدا نکردی؟</h3>
<p className="text-gray-400 text-xs">ساخت صدای اختصاصی با آپلود فایل</p>
</div>
<div className="w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center group-hover:bg-white/20 transition-colors">
<i className="fas fa-arrow-left text-white"></i>
</div>
</div>
</div>
{/* Category Tabs */}
<div className="flex overflow-x-auto pb-4 gap-3 mb-2 px-1 no-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<button
onClick={() => setActiveTab('all')}
className={`flex-shrink-0 px-5 py-2.5 rounded-full text-sm font-bold transition-all duration-300 border
${activeTab === 'all'
? 'bg-gradient-to-r from-primary to-secondary text-white border-transparent shadow-lg shadow-indigo-200'
: 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'}`}
>
همه
</button>
{categories.map((cat) => (
<button
key={cat.key}
onClick={() => setActiveTab(cat.key)}
className={`flex-shrink-0 px-4 py-2.5 rounded-full text-sm font-bold transition-all duration-300 border flex items-center gap-2
${activeTab === cat.key
? 'bg-gradient-to-r from-primary to-secondary text-white border-transparent shadow-lg shadow-indigo-200'
: 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'}`}
>
<i className={`fas ${cat.icon} text-xs`}></i>
{cat.title}
</button>
))}
</div>
{/* Grid Content */}
<div className="min-h-[300px]">
{categories.map((category) => {
if (activeTab !== 'all' && activeTab !== category.key) return null;
const categoryModels = MODELS.filter(m => m.category === category.key);
const isAlpha = category.key === 'alpha';
return (
<div key={category.key} className="mb-8 animate-[fadeIn_0.5s_ease-out]">
<div className="flex items-center gap-2 mb-4 px-2">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-white shadow-sm
${activeTab === category.key ? 'bg-secondary' : 'bg-gray-300'}`}>
<i className={`fas ${category.icon} text-xs`}></i>
</div>
<h2 className="text-lg font-black text-gray-800">{category.title}</h2>
{activeTab === 'all' && <div className="h-px bg-gray-100 flex-1 ml-4"></div>}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{categoryModels.map(renderModelCard)}
{/* Special Logic: User Custom Models next to Maryam (Alpha) */}
{isAlpha && customModels.map(renderModelCard)}
{/* PLUS BUTTON */}
{isAlpha && (
<div
onClick={onAddNewModel}
className="relative group cursor-pointer rounded-2xl aspect-square border-4 border-dashed border-gray-200 flex flex-col items-center justify-center gap-3 transition-all duration-300 hover:border-primary/50 hover:bg-primary/5 active:scale-95"
>
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white shadow-lg shadow-primary/20 group-hover:scale-110 transition-transform">
<i className="fas fa-plus text-2xl animate-[pulse_2s_infinite]"></i>
</div>
<span className="text-[10px] font-black text-gray-500 group-hover:text-primary transition-colors text-center px-1">ساخت مدل صدای دلخواه</span>
<div className="absolute top-2 right-2 flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</div>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default ModelGrid;