feat: Add project modal component and integrate it into the product grid.
This commit is contained in:
@@ -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={{
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
165
src/components/ProjectModal.jsx
Normal file
165
src/components/ProjectModal.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user