feat: Add project modal component and integrate it into the product grid.

This commit is contained in:
2025-12-15 19:52:31 +00:00
parent f9ff3383a7
commit 8dd658f206
3 changed files with 255 additions and 78 deletions

View File

@@ -79,11 +79,10 @@ function App() {
<div style={{
position: 'relative', // Changed to relative to sit on top of canvas if needed, or maintain z-index structure
zIndex: 1,
zIndex: 10,
pointerEvents: 'none',
textAlign: 'center',
mixBlendMode: 'difference', // Ensure text pops against wireframe
color: '#fff'
color: '#000' // Solid black text to sit on top of wireframe
}}>
<div style={{ overflow: 'hidden' }}>
<h1 className="hero-title" style={{

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import ProjectModal from './ProjectModal';
import '../styles/variables.css';
gsap.registerPlugin(ScrollTrigger);
@@ -16,6 +17,7 @@ const ProductGrid = () => {
const gridRef = useRef(null);
const titleRef = useRef(null);
const itemRefs = useRef([]);
const [selectedProject, setSelectedProject] = useState(null);
useEffect(() => {
const ctx = gsap.context(() => {
@@ -65,86 +67,97 @@ const ProductGrid = () => {
};
return (
<section id="work" ref={gridRef} style={{
padding: '100px 20px',
background: '#fff',
minHeight: '100vh'
}}>
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
<div style={{
marginBottom: '60px',
borderBottom: '1px solid #000',
paddingBottom: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline'
}} ref={titleRef}>
<h2 className="uppercase" style={{ fontSize: '2rem', margin: 0 }}>Selected Work</h2>
<span className="mono" style={{ fontSize: '0.9rem', color: '#666' }}>DESIGN / CODE</span>
</div>
<>
<section id="work" ref={gridRef} style={{
padding: '100px 20px',
background: '#fff',
minHeight: '100vh'
}}>
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
<div style={{
marginBottom: '60px',
borderBottom: '1px solid #000',
paddingBottom: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline'
}} ref={titleRef}>
<h2 className="uppercase" style={{ fontSize: '2rem', margin: 0 }}>Selected Work</h2>
<span className="mono" style={{ fontSize: '0.9rem', color: '#666' }}>DESIGN / CODE</span>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
gap: '2px', // Tight gap for grid lines effect
background: '#ccc', // Color of grid lines
border: '1px solid #ccc'
}}>
{products.map((p, i) => (
<div
key={p.id}
ref={el => itemRefs.current[i] = el}
className="product-item"
style={{
background: '#f5f5f5',
height: '450px',
padding: '30px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden'
}}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%', zIndex: 2 }}>
<span className="mono" style={{ fontSize: '0.8rem', color: '#ff4d00' }}>0{p.id}</span>
<div className="indicator" style={{
width: '8px',
height: '8px',
background: '#ccc',
borderRadius: '50%'
}}></div>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
gap: '2px', // Tight gap for grid lines effect
background: '#ccc', // Color of grid lines
border: '1px solid #ccc'
}}>
{products.map((p, i) => (
<div
key={p.id}
ref={el => itemRefs.current[i] = el}
className="product-item"
style={{
background: '#f5f5f5',
height: '450px',
padding: '30px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden'
}}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
onClick={() => setSelectedProject(p)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%', zIndex: 2 }}>
<span className="mono" style={{ fontSize: '0.8rem', color: '#ff4d00' }}>0{p.id}</span>
<div className="indicator" style={{
width: '8px',
height: '8px',
background: '#ccc',
borderRadius: '50%'
}}></div>
</div>
{/* Placeholder for Product Image */}
<div className="product-img" style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '5rem',
color: '#e0e0e0',
fontWeight: 800,
userSelect: 'none'
}}>
MJ
</div>
{/* Placeholder for Product Image */}
<div className="product-img" style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '5rem',
color: '#e0e0e0',
fontWeight: 800,
userSelect: 'none'
}}>
MJ
</div>
<div style={{ zIndex: 2 }}>
<h3 style={{ fontSize: '1.5rem', marginBottom: '5px' }}>{p.name}</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem', color: '#666' }}>
<span>{p.desc}</span>
<span>{p.price}</span>
<div style={{ zIndex: 2 }}>
<h3 style={{ fontSize: '1.5rem', marginBottom: '5px' }}>{p.name}</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem', color: '#666' }}>
<span>{p.desc}</span>
<span>{p.price}</span>
</div>
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
</section>
</section>
{/* Modal */}
{selectedProject && (
<ProjectModal
project={selectedProject}
onClose={() => setSelectedProject(null)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,165 @@
import React, { useEffect, useRef } from 'react';
import gsap from 'gsap';
const ProjectModal = ({ project, onClose }) => {
const modalRef = useRef(null);
const overlayRef = useRef(null);
const contentRef = useRef(null);
useEffect(() => {
const ctx = gsap.context(() => {
// Animation In
gsap.fromTo(overlayRef.current,
{ opacity: 0 },
{ opacity: 1, duration: 0.5, ease: "power2.out" }
);
gsap.fromTo(contentRef.current,
{ y: 100, opacity: 0, scale: 0.95 },
{ y: 0, opacity: 1, scale: 1, duration: 0.5, delay: 0.1, ease: "power3.out" }
);
}, modalRef);
// Prevent body scroll
document.body.style.overflow = 'hidden';
return () => {
ctx.revert();
document.body.style.overflow = ''; // Restore scroll
};
}, []);
const handleClose = () => {
// Animation Out manually before unmounting
gsap.to(contentRef.current, { y: 50, opacity: 0, duration: 0.3, ease: "power2.in" });
gsap.to(overlayRef.current, { opacity: 0, duration: 0.3, onComplete: onClose });
};
if (!project) return null;
return (
<div ref={modalRef} style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'auto'
}}>
{/* Overlay */}
<div
ref={overlayRef}
onClick={handleClose}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.8)',
backdropFilter: 'blur(5px)',
cursor: 'pointer'
}}
/>
{/* Modal Content */}
<div ref={contentRef} style={{
width: '90%',
maxWidth: '1000px',
height: '80vh',
background: '#e4e4e4',
position: 'relative',
display: 'flex',
flexDirection: 'column', // Stack on mobile, row on desktop (handled below)
overflow: 'hidden',
boxShadow: '0 20px 50px rgba(0,0,0,0.5)'
}}>
{/* Close Button */}
<button onClick={handleClose} style={{
position: 'absolute',
top: '20px',
right: '20px',
zIndex: 10,
background: 'transparent',
border: 'none',
fontSize: '1.5rem',
cursor: 'pointer',
color: '#000',
fontWeight: 'bold'
}}>
</button>
<div style={{ display: 'flex', height: '100%', flexDirection: 'row' }}>
{/* Left: Image / Visual */}
<div style={{
flex: '1.5',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRight: '1px solid #ccc'
}}>
<h1 style={{ fontSize: '8vw', color: '#f0f0f0', fontWeight: '900' }}>MJ</h1>
</div>
{/* Right: Info */}
<div style={{
flex: '1',
padding: '60px 40px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
overflowY: 'auto'
}}>
<div>
<span className="mono" style={{ color: '#ff4d00', marginBottom: '10px', display: 'block' }}>
{project.price} {/* Using 'price' for Year based on previous data struct */}
</span>
<h2 className="uppercase" style={{ fontSize: '3rem', lineHeight: 1, marginBottom: '20px' }}>
{project.name}
</h2>
<p style={{ fontSize: '1.1rem', color: '#444', marginBottom: '40px', lineHeight: 1.6 }}>
This is a detailed description of the {project.name} project.
It explores the intersection of design and technology,
focusing on user experience and visual impact.
</p>
<div className="mono" style={{ fontSize: '0.9rem', color: '#666' }}>
<h4 style={{ color: '#000', marginBottom: '10px' }}>Role</h4>
<p>Design, Development</p>
<br />
<h4 style={{ color: '#000', marginBottom: '10px' }}>Tech Stack</h4>
<p>React, Three.js, GSAP</p>
</div>
</div>
<button style={{
alignSelf: 'flex-start',
background: '#000',
color: '#fff',
border: 'none',
padding: '15px 30px',
textTransform: 'uppercase',
fontSize: '0.9rem',
cursor: 'pointer',
marginTop: '40px',
transition: 'background 0.2s'
}}
onMouseEnter={(e) => e.target.style.background = '#ff4d00'}
onMouseLeave={(e) => e.target.style.background = '#000'}
>
View Live
</button>
</div>
</div>
</div>
</div>
);
};
export default ProjectModal;