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