440 lines
20 KiB
TypeScript
440 lines
20 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
Play,
|
|
X,
|
|
ExternalLink,
|
|
Video,
|
|
Maximize2,
|
|
Tv,
|
|
Monitor
|
|
} from 'lucide-react';
|
|
import { PageHeader } from '../components/ui/PageHeader';
|
|
import { PageLayout } from '../components/layout/PageLayout';
|
|
import { getShowcaseContent } from '../services/ecosystem-api';
|
|
import type { ContentShowcase } from '../services/ecosystem-api';
|
|
|
|
// ── Helper: Extract YouTube video ID ──
|
|
const extractYouTubeVideoId = (url: string): string | null => {
|
|
const patterns = [
|
|
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
|
/(?:youtube-nocookie\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
|
|
];
|
|
for (const pattern of patterns) {
|
|
const match = url.match(pattern);
|
|
if (match) return match[1];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// ── Helper: Extract Instagram post/reel ID ──
|
|
const extractInstagramId = (url: string): string | null => {
|
|
const patterns = [
|
|
/instagram\.com\/p\/([a-zA-Z0-9_-]+)/,
|
|
/instagram\.com\/reel\/([a-zA-Z0-9_-]+)/,
|
|
/instagram\.com\/tv\/([a-zA-Z0-9_-]+)/
|
|
];
|
|
for (const pattern of patterns) {
|
|
const match = url.match(pattern);
|
|
if (match) return match[1];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// ── YouTube Player Embed Component ──
|
|
const YouTubeEmbed: React.FC<{ videoId: string }> = ({ videoId }) => {
|
|
return (
|
|
<div className="w-full aspect-video bg-black rounded-2xl overflow-hidden shadow-2xl relative">
|
|
<iframe
|
|
src={`https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&modestbranding=1`}
|
|
className="w-full h-full border-0 absolute inset-0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
allowFullScreen
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── Instagram Player Embed Component ──
|
|
const InstagramEmbed: React.FC<{ postId: string; size: 'compact' | 'theater' | 'cinema' }> = ({ postId, size }) => {
|
|
const maxWidthClass = size === 'compact' ? 'max-w-[360px]' : size === 'theater' ? 'max-w-[420px]' : 'max-w-[480px]';
|
|
return (
|
|
<div className={`w-full ${maxWidthClass} aspect-[9/16] max-h-full mx-auto bg-ink-950 rounded-2xl overflow-hidden shadow-2xl flex justify-center items-center`}>
|
|
<iframe
|
|
src={`https://www.instagram.com/p/${postId}/embed`}
|
|
className="w-full h-full border-0"
|
|
allowFullScreen
|
|
scrolling="no"
|
|
allow="autoplay; clipboard-write; encrypted-media; picture-in-picture"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── Twitter/X Embed Component ──
|
|
const TwitterEmbed: React.FC<{ url: string }> = ({ url }) => {
|
|
useEffect(() => {
|
|
if (!document.getElementById('twitter-wjs')) {
|
|
const script = document.createElement('script');
|
|
script.id = 'twitter-wjs';
|
|
script.src = 'https://platform.twitter.com/widgets.js';
|
|
script.async = true;
|
|
script.charset = 'utf-8';
|
|
document.body.appendChild(script);
|
|
} else {
|
|
try {
|
|
(window as any).twttr?.widgets?.load();
|
|
} catch (err) {
|
|
console.error('Failed to reload twitter widgets:', err);
|
|
}
|
|
}
|
|
}, [url]);
|
|
|
|
return (
|
|
<div className="w-full max-h-full overflow-y-auto flex justify-center bg-white p-4 sm:p-6 rounded-2xl">
|
|
<blockquote className="twitter-tweet" data-align="center">
|
|
<a href={url}>Loading Tweet...</a>
|
|
</blockquote>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── Collapsible Video Description Component ──
|
|
const VideoDescription: React.FC<{ text: string }> = ({ text }) => {
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const shouldCollapse = text.length > 180 || text.includes('\n');
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<p
|
|
className={`text-[11px] font-medium text-ink-500 leading-relaxed transition-all duration-300 ${
|
|
isExpanded ? '' : 'line-clamp-3'
|
|
}`}
|
|
style={{ whiteSpace: isExpanded ? 'pre-wrap' : 'normal' }}
|
|
>
|
|
{text}
|
|
</p>
|
|
{shouldCollapse && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsExpanded(!isExpanded);
|
|
}}
|
|
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"
|
|
>
|
|
{isExpanded ? 'Show Less' : 'Show More'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const ShowcasePage: React.FC = () => {
|
|
const [items, setItems] = useState<ContentShowcase[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [playingVideoId, setPlayingVideoId] = useState<string | null>(null);
|
|
|
|
// Resizable Lightbox state: compact | theater | cinema
|
|
const [lightboxSize, setLightboxSize] = useState<'compact' | 'theater' | 'cinema'>('compact');
|
|
|
|
useEffect(() => {
|
|
const fetchItems = async () => {
|
|
try {
|
|
const data = await getShowcaseContent();
|
|
setItems(data);
|
|
} catch (err) {
|
|
console.error('Failed to load showcase content:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchItems();
|
|
}, []);
|
|
|
|
const activeItem = items.find(item => item.id === playingVideoId);
|
|
|
|
const headerNode = (
|
|
<PageHeader
|
|
title="Featured Content"
|
|
subtitle="Discover interactive walk-throughs, demo reels, and case study updates across all Tech4Biz channels."
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<PageLayout header={headerNode}>
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex justify-between items-center border-b border-ink-200 pb-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="p-2 rounded-xl bg-blue-500/10 text-blue-600">
|
|
<Video className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-sm font-black uppercase tracking-wider text-ink-900">Media Showcase</h3>
|
|
<p className="text-[10px] font-bold text-ink-500 uppercase tracking-widest mt-0.5">YouTube, Instagram & Twitter/X Gallery</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-[10px] font-bold text-ink-400 uppercase tracking-widest">
|
|
{items.length} Videos Available
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex flex-1 items-center justify-center py-20">
|
|
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin"></div>
|
|
</div>
|
|
) : items.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-20 text-center border border-dashed border-ink-300 rounded-3xl p-8 bg-ink-50">
|
|
<Play className="w-12 h-12 text-ink-300 mb-3" />
|
|
<h4 className="text-sm font-black text-ink-900 uppercase tracking-wider">No Video Showcase Content</h4>
|
|
<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">
|
|
{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);
|
|
|
|
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>
|
|
)}
|
|
|
|
{/* 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} />
|
|
)}
|
|
</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>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Immersive Lightbox Media Modal ── */}
|
|
<AnimatePresence>
|
|
{activeItem && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-950/90 backdrop-blur-xl"
|
|
onClick={() => setPlayingVideoId(null)}
|
|
>
|
|
{/* Modal Container */}
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
transition={{ type: 'spring', damping: 25, stiffness: 250 }}
|
|
className={`relative bg-ink-900 border border-ink-800 rounded-3xl overflow-hidden shadow-2xl flex flex-col transition-all duration-300 ${
|
|
lightboxSize === 'cinema'
|
|
? 'w-[95vw] max-w-7xl md:flex-col h-[85vh] md:h-[90vh]'
|
|
: lightboxSize === 'theater'
|
|
? 'w-[95vw] md:w-[85vw] max-w-6xl md:flex-row h-[85vh] md:max-h-[85vh]'
|
|
: 'w-[95vw] md:w-full max-w-4xl md:flex-row h-[85vh] md:max-h-[80vh]'
|
|
}`}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Media Container */}
|
|
<div className="flex-1 bg-black flex items-center justify-center p-3 sm:p-4 overflow-hidden relative min-h-[180px] sm:min-h-[240px] md:min-h-[300px]">
|
|
{(() => {
|
|
const ytId = extractYouTubeVideoId(activeItem.youtubeUrl);
|
|
if (ytId) {
|
|
return <YouTubeEmbed videoId={ytId} />;
|
|
}
|
|
const igId = extractInstagramId(activeItem.youtubeUrl);
|
|
if (igId) {
|
|
return <InstagramEmbed postId={igId} size={lightboxSize} />;
|
|
}
|
|
if (activeItem.youtubeUrl.includes('twitter.com') || activeItem.youtubeUrl.includes('x.com')) {
|
|
return <TwitterEmbed url={activeItem.youtubeUrl} />;
|
|
}
|
|
return (
|
|
<div className="text-ink-400 text-xs font-bold p-10">
|
|
Unsupported media format. Please visit direct link.
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
{/* Info Container */}
|
|
<div className={`p-5 sm:p-6 flex flex-col justify-between border-ink-800 bg-ink-950 overflow-y-auto ${
|
|
lightboxSize === 'cinema'
|
|
? 'w-full border-t h-[40%] md:h-[30%] shrink-0'
|
|
: 'w-full md:w-80 border-t md:border-t-0 md:border-l h-[45%] md:h-auto shrink-0'
|
|
}`}>
|
|
<div className="space-y-4">
|
|
{/* Toolbar with Resizer Preset Buttons */}
|
|
<div className="flex justify-between items-center pb-2 border-b border-ink-800/60">
|
|
<div className="hidden md:flex bg-ink-900 p-0.5 rounded-lg border border-ink-800 gap-0.5">
|
|
<button
|
|
onClick={() => setLightboxSize('compact')}
|
|
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${
|
|
lightboxSize === 'compact'
|
|
? 'bg-ink-800 text-ink-0'
|
|
: 'text-ink-500 hover:text-ink-300'
|
|
}`}
|
|
title="Compact View"
|
|
>
|
|
<Monitor className="w-3 h-3" />
|
|
<span>Compact</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setLightboxSize('theater')}
|
|
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${
|
|
lightboxSize === 'theater'
|
|
? 'bg-ink-800 text-ink-0'
|
|
: 'text-ink-500 hover:text-ink-300'
|
|
}`}
|
|
title="Theater View"
|
|
>
|
|
<Tv className="w-3 h-3" />
|
|
<span>Theater</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setLightboxSize('cinema')}
|
|
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${
|
|
lightboxSize === 'cinema'
|
|
? 'bg-ink-800 text-ink-0'
|
|
: 'text-ink-500 hover:text-ink-300'
|
|
}`}
|
|
title="Cinema View"
|
|
>
|
|
<Maximize2 className="w-3 h-3" />
|
|
<span>Cinema</span>
|
|
</button>
|
|
</div>
|
|
<button
|
|
onClick={() => setPlayingVideoId(null)}
|
|
className="text-ink-400 hover:text-ink-0 transition-colors p-1.5 rounded-lg hover:bg-ink-850 cursor-pointer ml-auto md:ml-0"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-start pt-1">
|
|
<span className="px-2 py-0.5 rounded bg-ink-800 text-ink-300 text-[9px] font-black uppercase tracking-widest">
|
|
{(() => {
|
|
if (activeItem.youtubeUrl.includes('youtube.com') || activeItem.youtubeUrl.includes('youtu.be')) return 'YouTube';
|
|
if (activeItem.youtubeUrl.includes('instagram.com')) return 'Instagram';
|
|
if (activeItem.youtubeUrl.includes('twitter.com') || activeItem.youtubeUrl.includes('x.com')) return 'Twitter / X';
|
|
return 'Media';
|
|
})()}
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="text-base font-black tracking-tight text-ink-0 leading-snug">
|
|
{activeItem.title}
|
|
</h3>
|
|
|
|
{activeItem.description && (
|
|
<div className="max-h-60 md:max-h-none overflow-y-auto pr-1">
|
|
<p className="text-xs font-medium text-ink-400 leading-relaxed whitespace-pre-wrap">
|
|
{activeItem.description}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{activeItem.redirectUrl && (
|
|
<div className="pt-6 border-t border-ink-800 mt-6">
|
|
<a
|
|
href={activeItem.redirectUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-xl text-xs font-black uppercase tracking-wider bg-ink-0 text-ink-900 hover:bg-ink-100 hover:shadow-lg transition-all duration-300 cursor-pointer"
|
|
>
|
|
<span>{activeItem.redirectLabel || 'Explore More'}</span>
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
};
|
|
|
|
export default ShowcasePage;
|