Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import ProductViewer from './components/ProductViewer'
import gsap from 'gsap'
import { ScrollTrigger, SplitText } from 'gsap/all'
import Showcase from './components/Showcase'
import Performance from './components/Performance'
import Features from './components/Features'
import Highlights from './components/Highlights'
import Footer from './components/Footer'

gsap.registerPlugin(ScrollTrigger)

Expand All @@ -14,6 +18,10 @@ const App = () => {
<Hero />
<ProductViewer />
<Showcase />
<Performance />
<Features />
<Highlights />
<Footer />
</main>
)
}
Expand Down
129 changes: 129 additions & 0 deletions src/components/Features.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Canvas } from '@react-three/fiber'
import StudioLights from './three/StudioLights.jsx'
import { features, featureSequence } from '../constants/index.js'
import clsx from 'clsx'
import { Suspense, useEffect, useRef } from 'react'
import { Html } from '@react-three/drei'
import MacbookModel from './models/Macbook.jsx'
import { useMediaQuery } from 'react-responsive'
import useMacbookStore from '../store/index.js'
import { useGSAP } from '@gsap/react'
import gsap from 'gsap'

const ModelScroll = () => {
const groupRef = useRef(null)
const isMobile = useMediaQuery({ query: '(max-width: 1024px)' })
const { setTexture } = useMacbookStore()

useEffect(() => {
featureSequence.forEach((feature) => {
const v = document.createElement('video')
Object.assign(v, {
src: feature.videoPath,
muted: true,
playsInline: true,
preload: 'auto',
crossOrigen: 'anonymous'
})
v.load()
})
}, [])
Comment on lines +18 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo crossOrigen and missing cleanup for preloaded videos.

  1. crossOrigen should be crossOrigin (HTML attribute spelling).
  2. The created video elements are never cleaned up, which could cause memory leaks if the component remounts.
Proposed fix
 	useEffect(() => {
+		const videos = []
 		featureSequence.forEach((feature) => {
 			const v = document.createElement('video')
 			Object.assign(v, {
 				src: feature.videoPath,
 				muted: true,
 				playsInline: true,
 				preload: 'auto',
-				crossOrigen: 'anonymous'
+				crossOrigin: 'anonymous'
 			})
 			v.load()
+			videos.push(v)
 		})
+		return () => {
+			videos.forEach((v) => {
+				v.src = ''
+				v.load()
+			})
+		}
 	}, [])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
featureSequence.forEach((feature) => {
const v = document.createElement('video')
Object.assign(v, {
src: feature.videoPath,
muted: true,
playsInline: true,
preload: 'auto',
crossOrigen: 'anonymous'
})
v.load()
})
}, [])
useEffect(() => {
const videos = []
featureSequence.forEach((feature) => {
const v = document.createElement('video')
Object.assign(v, {
src: feature.videoPath,
muted: true,
playsInline: true,
preload: 'auto',
crossOrigin: 'anonymous'
})
v.load()
videos.push(v)
})
return () => {
videos.forEach((v) => {
v.src = ''
v.load()
})
}
}, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Features.jsx` around lines 18 - 30, Fix the typo and add
proper cleanup for preloaded videos: in the useEffect that iterates
featureSequence, change the video attribute name from crossOrigen to crossOrigin
and keep references to created elements (the const v video nodes) in an array;
after calling v.load() push each v into that array and return a cleanup function
from useEffect that iterates the array to pause(), removeAttribute('src'), call
load() to clear, and remove each node from the DOM to avoid leaks. Ensure you
update the useEffect closure (featureSequence and created videos) accordingly so
cleanup runs on unmount/remount.


useGSAP(() => {
const modelTimeLine = gsap.timeline({
scrollTrigger: {
trigger: '#f-canvas',
start: 'top top',
end: 'bottom top',
scrub: 1,
pin: true
}
})

const timeline = gsap.timeline({
scrollTrigger: {
trigger: '#f-canvas',
start: 'top center',
end: 'bottom top',
scrub: 1
}
})

if (groupRef.current) {
modelTimeLine.to(groupRef.current.rotation, {
y: Math.PI * 2,
ease: 'power1.inOut'
})
}

timeline
.call(() => setTexture('/videos/feature-1.mp4'))
.to('.box1', { opacity: 1, y: 0, delay: 1 })

.call(() => setTexture('/videos/feature-2.mp4'))
.to('.box2', { opacity: 1, y: 0 })

.call(() => setTexture('/videos/feature-3.mp4'))
.to('.box3', { opacity: 1, y: 0 })

.call(() => setTexture('/videos/feature-4.mp4'))
.to('.box4', { opacity: 1, y: 0 })

.call(() => setTexture('/videos/feature-5.mp4'))
.to('.box5', { opacity: 1, y: 0 })
}, [])

return (
<group ref={groupRef}>
<Suspense
fallback={
<Html>
<h1 className="text-white text-3xl uppercase">Loading...</h1>
</Html>
}
>
<MacbookModel
scale={isMobile ? 0.05 : 0.08}
position={[0, -1, 0]}
/>
</Suspense>
</group>
)
}

const Features = () => {
return (
<section id="features">
<h2>See it all in a new light.</h2>

<Canvas
id="f-canvas"
camera={{}}
>
<StudioLights />
<ambientLight intensity={0.5} />
<ModelScroll />
</Canvas>

<div className="absolute inset-0">
{features.map((feature, index) => (
<div
key={feature.id}
className={clsx('box', `box${index + 1}`, feature.styles)}
>
<img
src={feature.icon}
alt={feature.highlight}
/>
<p>
<span className="text-white">{feature.highlight}</span>
{feature.text}
</p>
</div>
))}
</div>
</section>
)
}

export default Features
34 changes: 34 additions & 0 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { footerLinks } from "../constants"

const Footer = () => {
return (
<footer>
<div className="'info">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in className breaks styling.

The className has an extra single quote: "'info" should be "info".

Proposed fix
-			<div className="'info">
+			<div className="info">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="'info">
<div className="info">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Footer.jsx` at line 6, In the Footer component fix the typo in
the div's className (currently "'info") by removing the stray single quote so
the className is "info"; locate the div element in Footer.jsx that renders the
container (the line with className="'info") and update it to the correct string
to restore styling.

<p>
More ways to shop: Find an Apple Store or other retailer near you. Or
call 000800 040 1966.
</p>
<img
src="/logo.svg"
alt="Apple logo"
/>
</div>

<hr />

<div className="links">
<p>Copyright © 2024 Apple Inc. All rights reserved.</p>

<ul>
{footerLinks.map(({ label, link }) => (
<li key={label}>
<a href={link}>{label}</a>
</li>
))}
</ul>
</div>
</footer>
)
}

export default Footer
71 changes: 71 additions & 0 deletions src/components/Highlights.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useGSAP } from '@gsap/react'
import { useMediaQuery } from 'react-responsive'
import gsap from 'gsap'

const Highlights = () => {
const isMobile = useMediaQuery({ query: '(max-width: 1024px)' })

useGSAP(() => {
gsap.to(['.left-column', '.right-column'], {
scrollTrigger: {
trigger: '#highlights',
start: isMobile ? 'bottom bottom ' : 'top top'
},
y: 0,
opacity: 1,
stagger: 0.5,
duration: 1,
ease: 'power1.inOut'
})
})
Comment on lines +8 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing dependency array causes stale isMobile value.

The useGSAP hook uses isMobile in the ScrollTrigger config but doesn't include it in a dependency array. If the viewport changes, the animation won't update to reflect the new start position.

Proposed fix
 	useGSAP(() => {
 		gsap.to(['.left-column', '.right-column'], {
 			scrollTrigger: {
 				trigger: '#highlights',
-				start: isMobile ? 'bottom bottom ' : 'top top'
+				start: isMobile ? 'bottom bottom' : 'top top'
 			},
 			y: 0,
 			opacity: 1,
 			stagger: 0.5,
 			duration: 1,
 			ease: 'power1.inOut'
 		})
-	})
+	}, { dependencies: [isMobile] })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useGSAP(() => {
gsap.to(['.left-column', '.right-column'], {
scrollTrigger: {
trigger: '#highlights',
start: isMobile ? 'bottom bottom ' : 'top top'
},
y: 0,
opacity: 1,
stagger: 0.5,
duration: 1,
ease: 'power1.inOut'
})
})
useGSAP(() => {
gsap.to(['.left-column', '.right-column'], {
scrollTrigger: {
trigger: '#highlights',
start: isMobile ? 'bottom bottom' : 'top top'
},
y: 0,
opacity: 1,
stagger: 0.5,
duration: 1,
ease: 'power1.inOut'
})
}, { dependencies: [isMobile] })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Highlights.jsx` around lines 8 - 20, The animation uses
isMobile inside the useGSAP callback (gsap.to([...], { scrollTrigger: { start:
isMobile ? 'bottom bottom ' : 'top top' } })) but useGSAP is invoked without
dependencies so the stale isMobile value persists; update the call so the effect
re-runs when isMobile changes—either pass isMobile in the dependency array to
useGSAP (e.g., useGSAP(..., [isMobile])) or modify useGSAP to accept and forward
a dependencies array, ensuring the gsap.to/ScrollTrigger config is recalculated
when isMobile toggles.


return (
<section id="highlights">
<h2>There’s never been a better time to upgrade.</h2>
<h3>Here’s what you get with the new MacBook Pro.</h3>

<div className="masonry">
<div className="left-column">
<div>
<img
src="/laptop.png"
alt="Laptop"
/>
<p>Fly through demanding tasks up to 9.8x faster.</p>
</div>
<div>
<img
src="/sun.png"
alt="Sun"
/>
<p>A stunning Liquid Retina XDR display.</p>
</div>
</div>
<div className="right-column">
<div className="apple-gradient">
<img
src="/ai.png"
alt="AI"
/>
<p>
Built for <br /> <span>Apple Intelligence.</span>
</p>
</div>
<div>
<img
src="/battery.png"
alt="Battery"
/>
<p>
Up to <span className="green-gradient"> 14 more hours </span>
battery life.{' '}
<span className="text-dark-100">(Up to 24 hours total.)</span>
</p>
</div>
</div>
</div>
</section>
)
}

export default Highlights
102 changes: 102 additions & 0 deletions src/components/Performance.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useRef } from 'react'
import { useGSAP } from '@gsap/react'
import { gsap } from 'gsap'
import {
performanceImages,
performanceImgPositions
} from '../constants/index.js'
import { useMediaQuery } from 'react-responsive'

const Performance = () => {
const isMobile = useMediaQuery({ query: '(max-width: 1024px)' })
const sectionRef = useRef(null)

useGSAP(
() => {
const sectionEl = sectionRef.current
if (!sectionEl) return

gsap.fromTo(
'.content p',
{ opacity: 0, y: 10 },
{
opacity: 1,
y: 0,
ease: 'power1.out',
scrollTrigger: {
trigger: '.content p',
start: 'top bottom',
end: 'top center',
scrub: true,
invalidateOnRefresh: true
}
}
)

if (isMobile) return

const tl = gsap.timeline({
defaults: { duration: 2, ease: 'power1.inOut', overwrite: 'auto' },
scrollTrigger: {
trigger: sectionEl,
start: 'top bottom',
end: 'bottom top',
scrub: 1,
invalidateOnRefresh: true
}
})

performanceImgPositions.forEach((item) => {
if (item.id === 'p5') return

const selector = `.${item.id}`
const vars = {}

if (typeof item.left === 'number') vars.left = `${item.left}%`
if (typeof item.right === 'number') vars.right = `${item.right}%`
if (typeof item.bottom === 'number') vars.bottom = `${item.bottom}%`

if (item.transform) vars.transform = item.transform

tl.to(selector, vars, 0)
})
},
{ scope: sectionRef, dependencies: [isMobile] }
)

return (
<section
id="performance"
ref={sectionRef}
>
<h2>Next-level graphics performance. Game on.</h2>

<div className="wrapper">
{performanceImages.map((item, index) => (
<img
key={index}
src={item.src}
className={item.id}
alt={item.alt || `Performance Image #${index + 1}`}
/>
))}
</div>

<div className="content">
<p>
Run graphics-intensive workflows with a responsiveness that keeps up
with your imagination. The M4 family of chips features a GPU with a
second-generation hardware-accelerated ray tracing engine that renders
images faster, so{' '}
<span className="text-white">
gaming feels more immersive and realistic than ever.
</span>{' '}
And Dynamic Caching optimizes fast on-chip memory to dramatically
increase average GPU utilization — driving a huge performance boost
for the most demanding pro apps and games.
</p>
</div>
</section>
)
}
export default Performance
Loading