feat(home): premium homepage redesign with procedural 3D hero - #36
Conversation
- Add a procedural react-three-fiber hero backdrop (HeroScene): glossy torus-knot + floating tech shapes, cursor parallax, inline Lightformer reflections. Lazy-loaded, GPU-tier gated, with reduced-motion / WebGL fallbacks so weak devices keep the CSS gradient hero. - Redesign Home: dark cinematic hero (white headline + gradient accent, glass trust card, stats), gradient value-prop and category cards, a clean CSS spotlight promo band (replaces the image carousel), refreshed testimonials and newsletter, an infinite brand marquee, and a soft page aurora. Light one-shot entrance animation (never leaves content hidden). - Polish shared ProductCard: white image area so product photos blend, a divider between media and content, image zoom on hover, stronger lift, and a gradient add-to-cart CTA. - Make the footer Insider-List form single-row (input + send inline). - Update Home and Footer snapshots; drop the obsolete banner test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Code Review
This pull request implements a home page redesign, replacing the image carousel with a procedural 3D backdrop (HeroScene) built with React Three Fiber for capable GPUs, alongside UI enhancements to the product cards, footer, and brand marquee. The review feedback highlights three key improvements: explicitly losing the temporary WebGL context during capability testing in Home.jsx to prevent GPU resource leaks, using a mutable ref for the onLost callback in HeroScene.jsx to avoid stale closures, and explicitly mocking the default export of HeroScene in Home.test.js to ensure compatibility with React.lazy.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let gl = null; | ||
| try { | ||
| const c = document.createElement('canvas'); | ||
| gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl'); | ||
| } catch (e) { | ||
| gl = null; | ||
| } | ||
| if (!gl) return 'none'; |
There was a problem hiding this comment.
When performing WebGL capability testing, creating a WebGL context allocates GPU resources. If these temporary contexts are not explicitly cleaned up, they can linger and cause context leaks, eventually hitting the browser's limit on active WebGL contexts. It is highly recommended to explicitly lose the temporary context using the WEBGL_lose_context extension once the capability check is complete.
| let gl = null; | |
| try { | |
| const c = document.createElement('canvas'); | |
| gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl'); | |
| } catch (e) { | |
| gl = null; | |
| } | |
| if (!gl) return 'none'; | |
| let gl = null; | |
| try { | |
| const c = document.createElement('canvas'); | |
| gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl'); | |
| } catch (e) { | |
| gl = null; | |
| } | |
| if (!gl) return 'none'; | |
| const loseContextExt = gl.getExtension('WEBGL_lose_context'); | |
| if (loseContextExt) { | |
| loseContextExt.loseContext(); | |
| } |
| const HeroScene = ({ quality = 'mid', onLost }) => { | ||
| const pointerRef = useRef({ x: 0, y: 0 }); | ||
| useEffect(() => { | ||
| if (typeof window === 'undefined') return undefined; | ||
| const onMove = e => { | ||
| pointerRef.current = { | ||
| x: (e.clientX / window.innerWidth) * 2 - 1, | ||
| y: -((e.clientY / window.innerHeight) * 2 - 1), | ||
| }; | ||
| }; | ||
| window.addEventListener('pointermove', onMove, { passive: true }); | ||
| return () => window.removeEventListener('pointermove', onMove); | ||
| }, []); | ||
|
|
||
| return ( | ||
| <Canvas | ||
| dpr={[1, quality === 'high' ? 1.8 : 1.3]} | ||
| camera={{ position: [0, 0, 8], fov: 42 }} | ||
| gl={{ antialias: quality === 'high', alpha: true, powerPreference: 'high-performance', failIfMajorPerformanceCaveat: false }} | ||
| onCreated={({ gl }) => { | ||
| gl.domElement.addEventListener('webglcontextlost', e => { e.preventDefault(); if (onLost) onLost(); }, { once: true }); | ||
| }} |
There was a problem hiding this comment.
The onLost callback is registered inside the onCreated listener of the Canvas, which only runs once during initialization. Since onLost is passed as an inline arrow function from the parent Home component, it changes on every render. This creates a stale closure bug where the event listener will always call the initial render's version of onLost. To prevent this, store onLost in a mutable ref and call the ref's current value inside the event listener.
const HeroScene = ({ quality = 'mid', onLost }) => {
const pointerRef = useRef({ x: 0, y: 0 });
const onLostRef = useRef(onLost);
useEffect(() => {
onLostRef.current = onLost;
}, [onLost]);
useEffect(() => {
if (typeof window === 'undefined') return undefined;
const onMove = e => {
pointerRef.current = {
x: (e.clientX / window.innerWidth) * 2 - 1,
y: -((e.clientY / window.innerHeight) * 2 - 1),
};
};
window.addEventListener('pointermove', onMove, { passive: true });
return () => window.removeEventListener('pointermove', onMove);
}, []);
return (
<Canvas
dpr={[1, quality === 'high' ? 1.8 : 1.3]}
camera={{ position: [0, 0, 8], fov: 42 }}
gl={{ antialias: quality === 'high', alpha: true, powerPreference: 'high-performance', failIfMajorPerformanceCaveat: false }}
onCreated={({ gl }) => {
gl.domElement.addEventListener('webglcontextlost', e => { e.preventDefault(); if (onLostRef.current) onLostRef.current(); }, { once: true });
}}| jest.mock('../assets/images/tech-gadgets.jpg', () => 'tech.jpg'); | ||
| jest.mock('../assets/images/trending-fashion.jpg', () => 'fashion.jpg'); | ||
| // Mock the 3D hero so tests never touch WebGL / three | ||
| jest.mock('../components/HeroScene', () => () => <div data-testid="hero-scene" />); |
There was a problem hiding this comment.
Since HeroScene is lazy-loaded using React.lazy, it expects the resolved module to have a .default property containing the component. Mocking the module directly as a function can cause runtime errors during tests depending on the test environment's module interop settings. Explicitly mock the default export to ensure robust compatibility with React.lazy.
jest.mock('../components/HeroScene', () => ({
__esModule: true,
default: () => <div data-testid="hero-scene" />,
}));
✅ Deploy Preview for mern-stack-ecommerce-website ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |

Summary
Complete redesign of the storefront homepage — a premium, professional, eye-catching landing page with a procedural 3D hero, while keeping all product/recommendation/newsletter functionality intact.
3D hero (
src/components/HeroScene.jsx).glb/.hdr/image assets.Home redesign (
src/pages/Home.jsx)Shared
ProductCardFooter
Notes
h1,h2,h3{color:#333}was overriding the hero's white text).Testing
jest→ 78 passed / 30 suites. Updated Home + Footer snapshots; removed the obsolete banner test.npm run buildcompiles (only the pre-existing@mediapipesource-map warning).🤖 Generated with Claude Code