Skip to content

Implement Performance Section#2

Merged
Ellhen merged 4 commits intomainfrom
macbook_app
Apr 6, 2026
Merged

Implement Performance Section#2
Ellhen merged 4 commits intomainfrom
macbook_app

Conversation

@Ellhen
Copy link
Copy Markdown
Owner

@Ellhen Ellhen commented Apr 6, 2026

Summary by CodeRabbit

  • New Features
    • Added three new content sections with scroll-driven animations for enhanced visual engagement
    • Enhanced 3D model with dynamic video textures and interactive effects
    • New footer section containing navigation links and brand information

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

The pull request adds four new section components (Performance, Features, Highlights, Footer) to the App, integrating scroll-driven GSAP animations and a responsive masonry layout. Additionally, the Macbook 3D model now supports dynamic video textures and runtime color updates via the store, with scroll-triggered texture changes in the Features section.

Changes

Cohort / File(s) Summary
App Structure
src/App.jsx
Integrated four new section components (Performance, Features, Highlights, Footer) into the main content area below Showcase.
Section Components
src/components/Performance.jsx, src/components/Features.jsx, src/components/Highlights.jsx, src/components/Footer.jsx
Added four new section components featuring scroll-driven GSAP animations, responsive layouts with useMediaQuery, canvas-based 3D rendering, video texture management, and dynamic link generation from constants.
3D Model Enhancement
src/components/models/Macbook.jsx
Integrated store-driven texture and color support; added video texture mapping via useVideoTexture hook to screen mesh, and implemented scene traversal with runtime material color updates excluding specified parts.
State Management
src/store/index.js
Extended Zustand store with texture state property and setTexture action; updated reset action to restore texture alongside color and scale properties.

Sequence Diagram

sequenceDiagram
    participant Scroll as User Scroll
    participant GSAP as GSAP ScrollTrigger
    participant Timeline as Animation Timeline
    participant Store as Macbook Store
    participant Model as Macbook 3D Model
    participant Mesh as Screen Mesh Material

    Scroll->>GSAP: Trigger scroll event in Features section
    GSAP->>Timeline: Execute timeline based on scroll position
    Timeline->>Store: Call setTexture('/videos/feature-N.mp4')
    Store->>Model: Update texture state
    Model->>Mesh: Apply new video texture via useVideoTexture
    Mesh->>Mesh: Render updated screen material on 3D model
    Timeline->>DOM: Fade and translate .boxN elements to opacity 1, y: 0
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Showcase section #1: Introduces Showcase component to App.jsx; this PR extends the App structure by adding additional section components in sequence, building on the established pattern of rendering major content sections.

Poem

🐰 Hopping through features, textures now flow,
Scroll-driven animations steal the show!
Canvas and models dance in the light,
Footer and highlights, bundled up tight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Implement Performance Section' refers to only one of five major components added in this changeset. While the Performance component is included, the PR also adds Features, Highlights, Footer components and updates the store and Macbook model—making the title incomplete and not representative of the full scope. Revise the title to reflect the complete changeset, such as 'Add Performance, Features, Highlights, and Footer sections with dynamic macbook rendering' or similar, to accurately convey all changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch macbook_app

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/components/models/Macbook.jsx (1)

1-6: Remove duplicate React import.

React is imported on line 1, and useEffect is imported separately on line 6. Consolidate into a single import.

Proposed fix
-import React from 'react'
+import React, { useEffect } from 'react'
 import { useGLTF, useVideoTexture } from '@react-three/drei'
 import useMacbookStore from '../../store'
 import { noChangeParts } from '../../constants/index.js'
 import { Color } from 'three'
-import { useEffect } from 'react'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/models/Macbook.jsx` around lines 1 - 6, The file imports React
twice (default import React and a separate named useEffect); consolidate to a
single import by combining useEffect into the React import (e.g., import React,
{ useEffect } from 'react') and remove the standalone useEffect import; update
any references relying on React/useEffect as needed in the Macbook component to
ensure they still work.
src/components/Performance.jsx (1)

49-50: Consider adding a comment explaining why p5 is skipped.

The exclusion of id === 'p5' from the animation timeline isn't self-documenting. A brief comment would help future maintainers understand the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Performance.jsx` around lines 49 - 50, Add an inline comment
inside the Performance.jsx loop where performanceImgPositions.forEach((item) =>
{ if (item.id === 'p5') return }) explaining why the element with id 'p5' is
intentionally skipped (e.g., reserved for a different animation, static element,
layout constraint, or handled elsewhere), referencing the
performanceImgPositions loop and the 'p5' id so future maintainers understand
the intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Features.jsx`:
- Around line 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.

In `@src/components/Footer.jsx`:
- 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.

In `@src/components/Highlights.jsx`:
- Around line 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.

In `@src/store/index.js`:
- Around line 13-14: The reset setter currently assigns texture
'./videos/feature-1.mp4' which is inconsistent with the initial texture
'/videos/feature-1.mp4' and will break video loading; update the reset()
implementation (the reset arrow function that calls set) to use the same
absolute path '/videos/feature-1.mp4' for the texture key so both the initial
state and the set/reset flow use the identical path.

---

Nitpick comments:
In `@src/components/models/Macbook.jsx`:
- Around line 1-6: The file imports React twice (default import React and a
separate named useEffect); consolidate to a single import by combining useEffect
into the React import (e.g., import React, { useEffect } from 'react') and
remove the standalone useEffect import; update any references relying on
React/useEffect as needed in the Macbook component to ensure they still work.

In `@src/components/Performance.jsx`:
- Around line 49-50: Add an inline comment inside the Performance.jsx loop where
performanceImgPositions.forEach((item) => { if (item.id === 'p5') return })
explaining why the element with id 'p5' is intentionally skipped (e.g., reserved
for a different animation, static element, layout constraint, or handled
elsewhere), referencing the performanceImgPositions loop and the 'p5' id so
future maintainers understand the intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2476b741-51b1-4433-9aed-b15cee43d3da

📥 Commits

Reviewing files that changed from the base of the PR and between 8c12825 and f6dbf74.

📒 Files selected for processing (7)
  • src/App.jsx
  • src/components/Features.jsx
  • src/components/Footer.jsx
  • src/components/Highlights.jsx
  • src/components/Performance.jsx
  • src/components/models/Macbook.jsx
  • src/store/index.js

Comment on lines +18 to +30
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()
})
}, [])
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.

Comment thread src/components/Footer.jsx
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.

Comment on lines +8 to +20
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'
})
})
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.

Comment on lines 8 to +23
export default function MacbookModel(props) {
const { nodes, materials } = useGLTF('/models/macbook-transformed.glb')
return (
<group {...props} dispose={null}>
<mesh geometry={nodes.Object_10.geometry} material={materials.PaletteMaterial001} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_16.geometry} material={materials.zhGRTuGrQoJflBD} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_20.geometry} material={materials.PaletteMaterial002} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_22.geometry} material={materials.lmWQsEjxpsebDlK} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_30.geometry} material={materials.LtEafgAVRolQqRw} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_32.geometry} material={materials.iyDJFXmHelnMTbD} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_34.geometry} material={materials.eJObPwhgFzvfaoZ} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_38.geometry} material={materials.nDsMUuDKliqGFdU} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_42.geometry} material={materials.CRQixVLpahJzhJc} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_48.geometry} material={materials.YYwBgwvcyZVOOAA} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_54.geometry} material={materials.SLGkCohDDelqXBu} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_58.geometry} material={materials.WnHKXHhScfUbJQi} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_66.geometry} material={materials.fNHiBfcxHUJCahl} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_74.geometry} material={materials.LpqXZqhaGCeSzdu} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_82.geometry} material={materials.gMtYExgrEUqPfln} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_96.geometry} material={materials.PaletteMaterial003} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_107.geometry} material={materials.JvMFZolVCdpPqjj} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_123.geometry} material={materials.sfCQkHOWyrsLmor} rotation={[Math.PI / 2, 0, 0]} />
<mesh geometry={nodes.Object_127.geometry} material={materials.ZCDwChwkbBfITSW} rotation={[Math.PI / 2, 0, 0]} />
</group>
)
const { color, texture } = useMacbookStore()

const { nodes, materials, scene } = useGLTF('/models/macbook-transformed.glb')

const screen = useVideoTexture(texture)

useEffect(() => {
scene.traverse((child) => {
if (child.isMesh) {
if (!noChangeParts.includes(child.name)) {
child.material.color = new Color(color)
}
}
})
}, [color, scene])
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does @react-three/drei useVideoTexture hook properly dispose of video elements when the src URL changes?

💡 Result:

No, the useVideoTexture hook from @react-three/drei does not properly dispose of video elements when the src URL changes. When the src changes, suspend-react (used internally for caching) treats it as a new key, creating a new video element and texture. The old video element remains in the suspend-react cache, continues playing, and is not paused, src-cleared, or removed. The cleanup useEffect only destroys HLS instances if present and does not handle the HTMLVideoElement or evict the cache. The VideoTexture component calls texture.dispose on unmount, but this only affects the THREE texture, not the video DOM element. This leads to memory leaks and multiple videos playing simultaneously. A related open GitHub issue (#2712, opened 2026-03-28) confirms this behavior with a reproduction repo, and a fix PR (#2714) is proposed but not yet merged as of 2026-04-06.

Citations:


Rapid texture changes cause memory leaks and simultaneous video playback.

The useVideoTexture hook from drei does not properly dispose of video elements when the src URL changes. When texture from the store changes rapidly (triggered by GSAP in Features.jsx), each new URL creates a new video element while old ones persist in the suspend-react cache, continue playing, and are never cleaned up. This causes memory leaks and multiple videos playing simultaneously. See pmndrs/drei#2712 and the proposed fix in pmndrs/drei#2714. Consider implementing a workaround or patching drei until the fix is merged.

Comment thread src/store/index.js
Comment on lines +13 to +14
reset: () =>
set({ color: '#2e2c2e', scale: 0.08, texture: './videos/feature-1.mp4' })
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 | 🟠 Major

Path inconsistency in reset() will break video texture loading.

The initial texture value uses an absolute path ('/videos/feature-1.mp4'), but reset() uses a relative path with a leading dot ('./videos/feature-1.mp4'). This inconsistency will cause the video texture to fail loading if reset() is ever called.

Proposed fix
 	reset: () =>
-		set({ color: '#2e2c2e', scale: 0.08, texture: './videos/feature-1.mp4' })
+		set({ color: '#2e2c2e', scale: 0.08, texture: '/videos/feature-1.mp4' })
📝 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
reset: () =>
set({ color: '#2e2c2e', scale: 0.08, texture: './videos/feature-1.mp4' })
reset: () =>
set({ color: '#2e2c2e', scale: 0.08, texture: '/videos/feature-1.mp4' })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/index.js` around lines 13 - 14, The reset setter currently assigns
texture './videos/feature-1.mp4' which is inconsistent with the initial texture
'/videos/feature-1.mp4' and will break video loading; update the reset()
implementation (the reset arrow function that calls set) to use the same
absolute path '/videos/feature-1.mp4' for the texture key so both the initial
state and the set/reset flow use the identical path.

@Ellhen Ellhen merged commit cc389fe into main Apr 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant