Refactor showcase cards to use absolute grid overlay expansion system
This commit is contained in:
parent
a1626a5c20
commit
9646309a0a
@ -100,8 +100,13 @@ const TwitterEmbed: React.FC<{ url: string }> = ({ url }) => {
|
||||
};
|
||||
|
||||
// ── Collapsible Video Description Component ──
|
||||
const VideoDescription: React.FC<{ text: string }> = ({ text }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
interface VideoDescriptionProps {
|
||||
text: string;
|
||||
isExpanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
}
|
||||
|
||||
const VideoDescription: React.FC<VideoDescriptionProps> = ({ text, isExpanded, onToggleExpand }) => {
|
||||
const shouldCollapse = text.length > 180 || text.includes('\n');
|
||||
|
||||
return (
|
||||
@ -118,7 +123,7 @@ const VideoDescription: React.FC<{ text: string }> = ({ text }) => {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
onToggleExpand();
|
||||
}}
|
||||
className="inline-flex items-center text-[10px] font-black uppercase tracking-wider text-blue-600 hover:text-blue-800 transition-colors focus:outline-none cursor-pointer"
|
||||
>
|
||||
@ -133,6 +138,7 @@ export const ShowcasePage: React.FC = () => {
|
||||
const [items, setItems] = useState<ContentShowcase[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [playingVideoId, setPlayingVideoId] = useState<string | null>(null);
|
||||
const [expandedItemId, setExpandedItemId] = useState<string | null>(null);
|
||||
|
||||
// Resizable Lightbox state: compact | theater | cinema
|
||||
const [lightboxSize, setLightboxSize] = useState<'compact' | 'theater' | 'cinema'>('compact');
|
||||
@ -189,98 +195,110 @@ export const ShowcasePage: React.FC = () => {
|
||||
<p className="text-xs text-ink-500 mt-1 max-w-sm">There are no featured videos available in the showcase right now. Check back later!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 animate-fadeIn">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 items-start animate-fadeIn">
|
||||
{items.map((item) => {
|
||||
const ytId = extractYouTubeVideoId(item.youtubeUrl);
|
||||
const isIg = item.youtubeUrl.includes('instagram.com');
|
||||
const isTw = item.youtubeUrl.includes('twitter.com') || item.youtubeUrl.includes('x.com');
|
||||
const thumbnail = item.thumbnailUrl || (ytId ? `https://img.youtube.com/vi/${ytId}/hqdefault.jpg` : null);
|
||||
const isExpanded = expandedItemId === item.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="group flex flex-col bg-ink-0 border border-ink-200 hover:border-ink-350 rounded-2xl overflow-hidden hover:shadow-xl transition-all duration-500"
|
||||
>
|
||||
{/* Video Player / Thumbnail */}
|
||||
<div className="relative aspect-video bg-ink-900 overflow-hidden">
|
||||
{thumbnail ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
/>
|
||||
) : (
|
||||
/* Platform Fallback Gradients */
|
||||
<div className={`w-full h-full flex items-center justify-center ${
|
||||
isIg
|
||||
? 'bg-gradient-to-tr from-yellow-500 via-pink-500 to-purple-600'
|
||||
: isTw
|
||||
? 'bg-ink-950'
|
||||
: 'bg-ink-100'
|
||||
}`}>
|
||||
{isIg && (
|
||||
<svg className="w-12 h-12 text-white/90 animate-pulse" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
|
||||
<path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" />
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5" />
|
||||
</svg>
|
||||
)}
|
||||
{isTw && (
|
||||
<svg className="w-12 h-12 text-white/90 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
)}
|
||||
{!isIg && !isTw && (
|
||||
<Play className="w-12 h-12 text-ink-400" />
|
||||
)}
|
||||
<div key={item.id} className="relative min-h-[400px]">
|
||||
<motion.div
|
||||
layout
|
||||
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
||||
className={`group flex flex-col bg-ink-0 border rounded-2xl overflow-hidden transition-all duration-300 ${
|
||||
isExpanded
|
||||
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-350 bg-ink-0'
|
||||
: 'relative w-full h-full border-ink-200 hover:border-ink-350 hover:shadow-xl'
|
||||
}`}
|
||||
>
|
||||
{/* Video Player / Thumbnail */}
|
||||
<div className="relative aspect-video bg-ink-900 overflow-hidden shrink-0">
|
||||
{thumbnail ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
/>
|
||||
) : (
|
||||
/* Platform Fallback Gradients */
|
||||
<div className={`w-full h-full flex items-center justify-center ${
|
||||
isIg
|
||||
? 'bg-gradient-to-tr from-yellow-500 via-pink-500 to-purple-600'
|
||||
: isTw
|
||||
? 'bg-ink-950'
|
||||
: 'bg-ink-100'
|
||||
}`}>
|
||||
{isIg && (
|
||||
<svg className="w-12 h-12 text-white/90 animate-pulse" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
|
||||
<path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" />
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5" />
|
||||
</svg>
|
||||
)}
|
||||
{isTw && (
|
||||
<svg className="w-12 h-12 text-white/90 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
)}
|
||||
{!isIg && !isTw && (
|
||||
<Play className="w-12 h-12 text-ink-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dark gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-ink-900/60 via-transparent to-transparent" />
|
||||
|
||||
{/* Play button overlay */}
|
||||
<button
|
||||
onClick={() => setPlayingVideoId(item.id)}
|
||||
className="absolute inset-0 flex items-center justify-center cursor-pointer group/play"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-full bg-ink-0/95 backdrop-blur-sm flex items-center justify-center shadow-2xl border border-ink-200/50 group-hover/play:scale-110 group-hover/play:bg-blue-600 group-hover/play:border-blue-500 transition-all duration-300">
|
||||
<Play className="w-6 h-6 text-ink-900 group-hover/play:text-ink-0 ml-0.5 transition-colors" fill="currentColor" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Platform Tag */}
|
||||
<div className="absolute bottom-2 right-2 px-2 py-0.5 rounded bg-ink-900/80 text-ink-0 text-[9px] font-bold backdrop-blur-sm">
|
||||
{isIg ? 'Instagram' : isTw ? 'Twitter / X' : 'YouTube'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dark gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-ink-900/60 via-transparent to-transparent" />
|
||||
|
||||
{/* Play button overlay */}
|
||||
<button
|
||||
onClick={() => setPlayingVideoId(item.id)}
|
||||
className="absolute inset-0 flex items-center justify-center cursor-pointer group/play"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-full bg-ink-0/95 backdrop-blur-sm flex items-center justify-center shadow-2xl border border-ink-200/50 group-hover/play:scale-110 group-hover/play:bg-blue-600 group-hover/play:border-blue-500 transition-all duration-300">
|
||||
<Play className="w-6 h-6 text-ink-900 group-hover/play:text-ink-0 ml-0.5 transition-colors" fill="currentColor" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Platform Tag */}
|
||||
<div className="absolute bottom-2 right-2 px-2 py-0.5 rounded bg-ink-900/80 text-ink-0 text-[9px] font-bold backdrop-blur-sm">
|
||||
{isIg ? 'Instagram' : isTw ? 'Twitter / X' : 'YouTube'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Info */}
|
||||
<div className="p-5 flex-1 flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-black tracking-tight text-ink-900 leading-snug">
|
||||
{item.title}
|
||||
</h3>
|
||||
{item.description && (
|
||||
<VideoDescription text={item.description} />
|
||||
{/* Content Info */}
|
||||
<div className="p-5 flex-grow flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-black tracking-tight text-ink-900 leading-snug">
|
||||
{item.title}
|
||||
</h3>
|
||||
{item.description && (
|
||||
<VideoDescription
|
||||
text={item.description}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={() => setExpandedItemId(isExpanded ? null : item.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Redirect CTA */}
|
||||
{item.redirectUrl && (
|
||||
<div className="pt-2">
|
||||
<a
|
||||
href={item.redirectUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-ink-700 hover:text-ink-900 transition-colors"
|
||||
>
|
||||
<span>{item.redirectLabel || 'Learn More'}</span>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Redirect CTA */}
|
||||
{item.redirectUrl && (
|
||||
<div className="pt-2">
|
||||
<a
|
||||
href={item.redirectUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-ink-700 hover:text-ink-900 transition-colors"
|
||||
>
|
||||
<span>{item.redirectLabel || 'Learn More'}</span>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user