Add video gallery with 5 community videos and archived board intro
This commit is contained in:
parent
a92839f08f
commit
e241e0946b
9 changed files with 276 additions and 2 deletions
BIN
client/public/videos/board-intro-archive.mp4
Normal file
BIN
client/public/videos/board-intro-archive.mp4
Normal file
Binary file not shown.
BIN
client/public/videos/community-01.mp4
Normal file
BIN
client/public/videos/community-01.mp4
Normal file
Binary file not shown.
BIN
client/public/videos/community-02.mp4
Normal file
BIN
client/public/videos/community-02.mp4
Normal file
Binary file not shown.
BIN
client/public/videos/community-03.mp4
Normal file
BIN
client/public/videos/community-03.mp4
Normal file
Binary file not shown.
BIN
client/public/videos/community-04.mp4
Normal file
BIN
client/public/videos/community-04.mp4
Normal file
Binary file not shown.
BIN
client/public/videos/community-05.mp4
Normal file
BIN
client/public/videos/community-05.mp4
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
270
client/src/components/VideoGallery.tsx
Normal file
270
client/src/components/VideoGallery.tsx
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue