Add video gallery with 5 community videos and archived board intro

This commit is contained in:
TheMaddax 2025-10-14 14:00:52 -06:00
parent a92839f08f
commit e241e0946b
9 changed files with 276 additions and 2 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -10,6 +10,7 @@ import Bylaws from './components/Bylaws';
import Sponsors from './components/Sponsors';
import MeetBoard from './components/Meet-Board';
import Gallery from './components/Gallery';
import VideoGallery from './components/VideoGallery';
/* import ComingSoon from './components/ComingSoon'; */
import JoinMCDi from './components/JoinMCDi';
@ -28,6 +29,7 @@ const App: React.FC = () => {
<Route path="/sponsors" element={<Sponsors />} />
<Route path="/meet-board" element={<MeetBoard />} />
<Route path="/gallery" element={<Gallery />} />
<Route path="/videos" element={<VideoGallery />} />
<Route path="/joinmcdi" element={<JoinMCDi />} />
</Routes>
</main>

View file

@ -74,7 +74,8 @@ const Header: React.FC = () => {
<li><NavItem href="/" onClick={() => setIsMenuOpen(false)}>Home</NavItem></li>
<li><NavItem href="/about" onClick={() => setIsMenuOpen(false)}>About MCDi</NavItem></li>
<li><NavItem href="/calendar" onClick={() => setIsMenuOpen(false)}>Calendar</NavItem></li>
<li><NavItem href="/gallery" onClick={() => setIsMenuOpen(false)}>Gallery</NavItem></li>
<li><NavItem href="/gallery" onClick={() => setIsMenuOpen(false)}>Photo Gallery</NavItem></li>
<li><NavItem href="/videos" onClick={() => setIsMenuOpen(false)}>Video Gallery</NavItem></li>
<li><NavItem href="/joinmcdi" onClick={() => setIsMenuOpen(false)}>Join MCDi</NavItem></li>
</ul>
</motion.div>
@ -157,7 +158,8 @@ const AboutDropdown: React.FC = () => {
<DropdownItem href="/meet-board">Meet MCDi Board</DropdownItem>
<DropdownItem href="/bylaws">Bylaws</DropdownItem>
<DropdownItem href="/minutes">Minutes</DropdownItem>
<DropdownItem href="/gallery">Gallery</DropdownItem>
<DropdownItem href="/gallery">Photo Gallery</DropdownItem>
<DropdownItem href="/videos">Video Gallery</DropdownItem>
<DropdownItem href="/sponsors">Sponsors</DropdownItem>
</div>
</motion.div>

View file

@ -0,0 +1,270 @@
import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FaPlay, FaPause, FaStop } from 'react-icons/fa';
interface VideoItem {
id: number;
videoUrl: string;
title: string;
thumbnail?: string;
}
const videoData: VideoItem[] = [
{
id: 1,
videoUrl: '/videos/community-01.mp4',
title: 'Community Event Video 1',
},
{
id: 2,
videoUrl: '/videos/community-02.mp4',
title: 'Community Event Video 2',
},
{
id: 3,
videoUrl: '/videos/community-03.mp4',
title: 'Community Event Video 3',
},
{
id: 4,
videoUrl: '/videos/community-04.mp4',
title: 'Community Event Video 4',
},
{
id: 5,
videoUrl: '/videos/community-05.mp4',
title: 'Community Event Video 5',
},
{
id: 6,
videoUrl: '/videos/board-intro-archive.mp4',
title: 'Board Introduction Archive',
},
];
const VideoGallery: React.FC = () => {
const [selectedVideo, setSelectedVideo] = useState<VideoItem | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [showControls, setShowControls] = useState(true);
const [progress, setProgress] = useState(0);
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const openVideo = (item: VideoItem) => {
setSelectedVideo(item);
setIsPlaying(false);
setProgress(0);
};
const closeVideo = () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.currentTime = 0;
}
setSelectedVideo(null);
setIsPlaying(false);
};
const togglePlay = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const handleStop = () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.currentTime = 0;
setIsPlaying(false);
}
};
const handleTimeUpdate = () => {
if (videoRef.current) {
const progress = (videoRef.current.currentTime / videoRef.current.duration) * 100;
setProgress(progress);
}
};
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
if (videoRef.current) {
const seekTime = (parseFloat(e.target.value) / 100) * videoRef.current.duration;
videoRef.current.currentTime = seekTime;
}
};
const handleMouseMove = () => {
setShowControls(true);
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
controlsTimeoutRef.current = setTimeout(() => {
if (isPlaying) {
setShowControls(false);
}
}, 3000);
};
useEffect(() => {
return () => {
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
};
}, []);
return (
<div className="container mx-auto px-4 py-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1 className="text-4xl font-bold text-[#8C1D40] mb-8">Video Gallery</h1>
{/* Video Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{videoData.map((item) => (
<motion.div
key={item.id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
whileHover={{ scale: 1.05 }}
className="cursor-pointer"
onClick={() => openVideo(item)}
>
<div className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300">
<div className="aspect-video bg-gray-200 flex items-center justify-center relative">
<video
src={item.videoUrl}
className="w-full h-full object-cover"
preload="metadata"
/>
<div className="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
<FaPlay className="text-white text-5xl opacity-80" />
</div>
</div>
<div className="p-3 bg-[#6B1631]">
<p className="text-yellow-400 text-sm font-semibold text-center">
{item.title}
</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Video Modal */}
<AnimatePresence>
{selectedVideo && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4"
onClick={closeVideo}
>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="relative max-w-5xl w-full"
onClick={(e) => e.stopPropagation()}
onMouseMove={handleMouseMove}
onMouseLeave={() => isPlaying && setShowControls(false)}
>
{/* Close Button */}
<button
onClick={closeVideo}
className="absolute -top-12 right-0 text-white hover:text-yellow-400 transition-colors duration-200 z-10"
aria-label="Close video"
>
<svg
className="w-10 h-10"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/* Video Container */}
<div className="bg-black rounded-lg overflow-hidden shadow-2xl border-4 border-[#6B1631]">
<video
ref={videoRef}
className="w-full"
onTimeUpdate={handleTimeUpdate}
onEnded={() => setIsPlaying(false)}
aria-label={selectedVideo.title}
>
<source src={selectedVideo.videoUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Video Controls */}
<AnimatePresence>
{showControls && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="bg-black bg-opacity-75 p-4"
>
<div className="flex items-center justify-between mb-2">
<p className="text-yellow-400 font-semibold">
{selectedVideo.title}
</p>
</div>
<div className="flex items-center">
<button
onClick={handleStop}
className="text-white hover:text-yellow-300 transition-colors mr-2 p-2"
aria-label="Stop video"
>
<FaStop size={20} />
</button>
<button
onClick={togglePlay}
className="text-white hover:text-yellow-300 transition-colors mr-2 p-2"
aria-label={isPlaying ? 'Pause video' : 'Play video'}
>
{isPlaying ? <FaPause size={20} /> : <FaPlay size={20} />}
</button>
<input
type="range"
min={0}
max={100}
value={progress}
onChange={handleSeek}
className="w-full mx-2 accent-yellow-300"
aria-label="Video progress"
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default VideoGallery;