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 Sponsors from './components/Sponsors';
|
||||||
import MeetBoard from './components/Meet-Board';
|
import MeetBoard from './components/Meet-Board';
|
||||||
import Gallery from './components/Gallery';
|
import Gallery from './components/Gallery';
|
||||||
|
import VideoGallery from './components/VideoGallery';
|
||||||
/* import ComingSoon from './components/ComingSoon'; */
|
/* import ComingSoon from './components/ComingSoon'; */
|
||||||
import JoinMCDi from './components/JoinMCDi';
|
import JoinMCDi from './components/JoinMCDi';
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ const App: React.FC = () => {
|
||||||
<Route path="/sponsors" element={<Sponsors />} />
|
<Route path="/sponsors" element={<Sponsors />} />
|
||||||
<Route path="/meet-board" element={<MeetBoard />} />
|
<Route path="/meet-board" element={<MeetBoard />} />
|
||||||
<Route path="/gallery" element={<Gallery />} />
|
<Route path="/gallery" element={<Gallery />} />
|
||||||
|
<Route path="/videos" element={<VideoGallery />} />
|
||||||
<Route path="/joinmcdi" element={<JoinMCDi />} />
|
<Route path="/joinmcdi" element={<JoinMCDi />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,8 @@ const Header: React.FC = () => {
|
||||||
<li><NavItem href="/" onClick={() => setIsMenuOpen(false)}>Home</NavItem></li>
|
<li><NavItem href="/" onClick={() => setIsMenuOpen(false)}>Home</NavItem></li>
|
||||||
<li><NavItem href="/about" onClick={() => setIsMenuOpen(false)}>About MCDi</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="/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>
|
<li><NavItem href="/joinmcdi" onClick={() => setIsMenuOpen(false)}>Join MCDi</NavItem></li>
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -157,7 +158,8 @@ const AboutDropdown: React.FC = () => {
|
||||||
<DropdownItem href="/meet-board">Meet MCDi Board</DropdownItem>
|
<DropdownItem href="/meet-board">Meet MCDi Board</DropdownItem>
|
||||||
<DropdownItem href="/bylaws">Bylaws</DropdownItem>
|
<DropdownItem href="/bylaws">Bylaws</DropdownItem>
|
||||||
<DropdownItem href="/minutes">Minutes</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>
|
<DropdownItem href="/sponsors">Sponsors</DropdownItem>
|
||||||
</div>
|
</div>
|
||||||
</motion.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