Add gallery system to the website
This commit is contained in:
parent
3294fc2cc6
commit
9733d4099f
4 changed files with 190 additions and 8 deletions
|
|
@ -9,6 +9,7 @@ import Footer from './components/Footer';
|
||||||
import Bylaws from './components/Bylaws';
|
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 ComingSoon from './components/ComingSoon'; */
|
/* import ComingSoon from './components/ComingSoon'; */
|
||||||
import JoinMCDi from './components/JoinMCDi';
|
import JoinMCDi from './components/JoinMCDi';
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ const App: React.FC = () => {
|
||||||
<Route path="/bylaws" element={<Bylaws />} />
|
<Route path="/bylaws" element={<Bylaws />} />
|
||||||
<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="/joinmcdi" element={<JoinMCDi />} />
|
<Route path="/joinmcdi" element={<JoinMCDi />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
165
client/src/components/Gallery.tsx
Normal file
165
client/src/components/Gallery.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
interface GalleryItem {
|
||||||
|
id: number;
|
||||||
|
imageUrl: string;
|
||||||
|
caption: string;
|
||||||
|
alt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder data using board member headshots
|
||||||
|
const galleryData: GalleryItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
imageUrl: '/headshots/tessa.jpg',
|
||||||
|
caption: 'Tessa Williams - President',
|
||||||
|
alt: 'Tessa Williams headshot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
imageUrl: '/headshots/rita.jpg',
|
||||||
|
caption: 'Rita Sommers-Flanagan - Vice President',
|
||||||
|
alt: 'Rita Sommers-Flanagan headshot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
imageUrl: '/headshots/aubz.jpg',
|
||||||
|
caption: 'Aubz Orr - Secretary',
|
||||||
|
alt: 'Aubz Orr headshot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
imageUrl: '/headshots/eliza.jpg',
|
||||||
|
caption: 'Eliza Gutierrez - Treasurer',
|
||||||
|
alt: 'Eliza Gutierrez headshot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
imageUrl: '/headshots/skyla.jpg',
|
||||||
|
caption: 'Skyla Heaton - Board Member',
|
||||||
|
alt: 'Skyla Heaton headshot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
imageUrl: '/headshots/kevin.jpg',
|
||||||
|
caption: 'Kevin Wolff - Board Member',
|
||||||
|
alt: 'Kevin Wolff headshot',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Gallery: React.FC = () => {
|
||||||
|
const [selectedImage, setSelectedImage] = useState<GalleryItem | null>(null);
|
||||||
|
|
||||||
|
const openImage = (item: GalleryItem) => {
|
||||||
|
setSelectedImage(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeImage = () => {
|
||||||
|
setSelectedImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
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">Photo Gallery</h1>
|
||||||
|
|
||||||
|
{/* Photo Grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
|
{galleryData.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={() => openImage(item)}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300">
|
||||||
|
<div className="aspect-square overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={item.imageUrl}
|
||||||
|
alt={item.alt}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#6B1631]">
|
||||||
|
<p className="text-yellow-400 text-sm font-semibold text-center truncate">
|
||||||
|
{item.caption}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Image Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedImage && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
|
||||||
|
onClick={closeImage}
|
||||||
|
>
|
||||||
|
<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 max-h-[90vh] flex flex-col"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
onClick={closeImage}
|
||||||
|
className="absolute -top-12 right-0 text-white hover:text-yellow-400 transition-colors duration-200 z-10"
|
||||||
|
aria-label="Close image"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Image Container */}
|
||||||
|
<div className="bg-white rounded-t-lg overflow-hidden shadow-2xl">
|
||||||
|
<img
|
||||||
|
src={selectedImage.imageUrl}
|
||||||
|
alt={selectedImage.alt}
|
||||||
|
className="max-w-full max-h-[70vh] w-auto h-auto mx-auto object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caption */}
|
||||||
|
<div className="bg-[#6B1631] rounded-b-lg p-4 border-2 border-black shadow-2xl">
|
||||||
|
<p className="text-yellow-400 text-xl font-bold text-center">
|
||||||
|
{selectedImage.caption}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gallery;
|
||||||
|
|
@ -74,6 +74,7 @@ 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="/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>
|
||||||
|
|
@ -156,6 +157,7 @@ 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="/sponsors">Sponsors</DropdownItem>
|
<DropdownItem href="/sponsors">Sponsors</DropdownItem>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,21 @@
|
||||||
# Active Context
|
# Active Context
|
||||||
|
|
||||||
## Current Task
|
## Current Task
|
||||||
Completed: Made yellow donation button visible on mobile header
|
Completed: Created new Gallery page with photo grid and modal viewer
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
1. Modified Header.tsx to display donation button on mobile:
|
1. Created new Gallery component (Gallery.tsx):
|
||||||
|
- Responsive photo grid layout (1-4 columns based on screen size)
|
||||||
|
- Clickable images that enlarge in a modal viewer
|
||||||
|
- Yellow text captions on red background with black border
|
||||||
|
- Smooth animations using Framer Motion
|
||||||
|
- Currently using board member headshots as placeholder images
|
||||||
|
- Modal features: click outside to close, X button, spring animation
|
||||||
|
- Maintains website's color scheme (#6B1631 red, yellow-400 text)
|
||||||
|
- Added to navigation: "About MCDi" dropdown and mobile menu
|
||||||
|
- Route added: /gallery
|
||||||
|
|
||||||
|
2. Previous: Modified Header.tsx to display donation button on mobile:
|
||||||
- Added permanent yellow donation button below hamburger menu on mobile devices
|
- Added permanent yellow donation button below hamburger menu on mobile devices
|
||||||
- Button is always visible in the red header area (no need to open menu)
|
- Button is always visible in the red header area (no need to open menu)
|
||||||
- Removed duplicate donation button from inside mobile menu dropdown
|
- Removed duplicate donation button from inside mobile menu dropdown
|
||||||
|
|
@ -35,11 +46,13 @@ Completed: Made yellow donation button visible on mobile header
|
||||||
- All previous changes successfully deployed to production
|
- All previous changes successfully deployed to production
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
1. Commit and push Header.tsx changes to Git repository
|
1. Wait for website owner to provide actual gallery photos
|
||||||
2. Deploy changes through Portainer (pull and redeploy)
|
2. Replace placeholder headshots with real gallery photos
|
||||||
3. Verify mobile donation button appears correctly on production
|
3. Update captions with appropriate descriptions
|
||||||
4. Verify desktop layout remains unchanged
|
4. Commit all changes (Gallery.tsx, App.tsx, Header.tsx) to Git
|
||||||
5. Test donation button functionality on mobile devices
|
5. Deploy changes through Portainer (pull and redeploy)
|
||||||
|
6. Test gallery functionality on production (desktop and mobile)
|
||||||
|
7. Consider adding video support to gallery in future
|
||||||
|
|
||||||
## Solution Summary
|
## Solution Summary
|
||||||
- Problem: Portainer repository deployments require stack.env file in Git repo
|
- Problem: Portainer repository deployments require stack.env file in Git repo
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue