<a href="https://youtu.be/kRQbRAJ4-Fs" target="_blank">
<img src="https://i.postimg.cc/37PnQw8n/Image-from.png" alt="Project Banner">
</a>
<img src="https://img.shields.io/badge/-React_JS-black?style=for-the-badge&logoColor=white&logo=react&color=61DAFB" alt="react.js" />
<img src="https://img.shields.io/badge/-Three_JS-black?style=for-the-badge&logoColor=white&logo=threedotjs&color=000000" alt="three.js" />
<img src="https://img.shields.io/badge/-GSAP-black?style=for-the-badge&logoColor=white&logo=greensock&color=88CE02" alt="greensock" />
<img src="https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4" alt="tailwindcss" />
Build this project step by step with our detailed tutorial on <a href="https://www.youtube.com/@javascriptmastery/videos" target="_blank"><b>JavaScript Mastery</b></a> YouTube. Join the JSM family!
This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, JavaScript Mastery.
If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner!
This is a clone of Apple's iPhone 15 Pro website using React.js and TailwindCSS. It highlights the effective use of GSAP (Greensock Animations) and Three.js for displaying iPhone 15 Pro models in various colors and shapes.
If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out.
👉 Beautiful Subtle Smooth Animations using GSAP: Enhanced user experience with seamless and captivating animations powered by GSAP.
👉 3D Model Rendering with Different Colors and Sizes: Explore the iPhone 15 Pro from every angle with dynamic 3D rendering, offering various color and size options.
👉 Custom Video Carousel (made with GSAP): Engage users with a unique and interactive video carousel developed using GSAP for a personalized browsing experience.
👉 Completely Responsive: Consistent access and optimal viewing on any device with a fully responsive design that adapts to different screen sizes.
and many more, including code architecture and reusability
Follow these steps to set up the project locally on your machine.
Prerequisites
Make sure you have the following installed on your machine:
Cloning the Repository
git clone https://github.com/JavaScript-Mastery-Pro/iphone-doc.git
cd iphone-doc
Installation
Install the project dependencies using npm:
npm install
Running the Project
npm run dev
Open http://localhost:5173 in your browser to view the project.
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
blue: "#2997FF",
gray: {
DEFAULT: "#86868b",
100: "#94928d",
200: "#afafaf",
300: "#42424570",
},
zinc: "#101010",
},
},
},
plugins: [],
};
constants/index.js
import {
blackImg,
blueImg,
highlightFirstVideo,
highlightFourthVideo,
highlightSecondVideo,
highlightThirdVideo,
whiteImg,
yellowImg,
} from "../utils";
export const navLists = ["Store", "Mac", "iPhone", "Support"];
export const hightlightsSlides = [
{
id: 1,
textLists: [
"Enter A17 Pro.",
"Game‑changing chip.",
"Groundbreaking performance.",
],
video: highlightFirstVideo,
videoDuration: 4,
},
{
id: 2,
textLists: ["Titanium.", "So strong. So light. So Pro."],
video: highlightSecondVideo,
videoDuration: 5,
},
{
id: 3,
textLists: [
"iPhone 15 Pro Max has the",
"longest optical zoom in",
"iPhone ever. Far out.",
],
video: highlightThirdVideo,
videoDuration: 2,
},
{
id: 4,
textLists: ["All-new Action button.", "What will yours do?."],
video: highlightFourthVideo,
videoDuration: 3.63,
},
];
export const models = [
{
id: 1,
title: "iPhone 15 Pro in Natural Titanium",
color: ["#8F8A81", "#ffe7b9", "#6f6c64"],
img: yellowImg,
},
{
id: 2,
title: "iPhone 15 Pro in Blue Titanium",
color: ["#53596E", "#6395ff", "#21242e"],
img: blueImg,
},
{
id: 3,
title: "iPhone 15 Pro in White Titanium",
color: ["#C9C8C2", "#ffffff", "#C9C8C2"],
img: whiteImg,
},
{
id: 4,
title: "iPhone 15 Pro in Black Titanium",
color: ["#454749", "#3b3b3b", "#181819"],
img: blackImg,
},
];
export const sizes = [
{ label: '6.1"', value: "small" },
{ label: '6.7"', value: "large" },
];
export const footerLinks = [
"Privacy Policy",
"Terms of Use",
"Sales Policy",
"Legal",
"Site Map",
];
Lights.jsx
import { Environment, Lightformer } from "@react-three/drei";
const Lights = () => {
return (
// group different lights and lightformers. We can use group to organize lights, cameras, meshes, and other objects in the scene.
<group name="lights">
{/**
* @description Environment is used to create a background environment for the scene
* https://github.com/pmndrs/drei?tab=readme-ov-file#environment
*/}
<Environment resolution={256}>
<group>
{/**
* @description Lightformer used to create custom lights with various shapes and properties in a 3D scene.
* https://github.com/pmndrs/drei?tab=readme-ov-file#lightformer
*/}
<Lightformer
form="rect"
intensity={10}
position={[-1, 0, -10]}
scale={10}
color={"#495057"}
/>
<Lightformer
form="rect"
intensity={10}
position={[-10, 2, 1]}
scale={10}
rotation-y={Math.PI / 2}
/>
<Lightformer
form="rect"
intensity={10}
position={[10, 0, 1]}
scale={10}
rotation-y={Math.PI / 2}
/>
</group>
</Environment>
{/**
* @description spotLight is used to create a light source positioned at a specific point
* in the scene that emits light in a specific direction.
* https://threejs.org/docs/#api/en/lights/SpotLight
*/}
<spotLight
position={[-2, 10, 5]}
angle={0.15}
penumbra={1} // the penumbra is the soft edge of a shadow cast by a point light
decay={0} // the amount the light dims as it moves away from the source
intensity={Math.PI * 0.2} // the light intensity
color={"#f8f9fa"}
/>
<spotLight
position={[0, -25, 10]}
angle={0.15}
penumbra={1}
decay={0}
intensity={Math.PI * 0.2}
color={"#f8f9fa"}
/>
<spotLight
position={[0, 15, 5]}
angle={0.15}
penumbra={1}
decay={0.1}
intensity={Math.PI * 3}
/>
</group>
);
};
export default Lights;
materials
useEffect(() => {
Object.entries(materials).map((material) => {
// these are the material names that can't be changed color
if (
material[0] !== "zFdeDaGNRwzccye" &&
material[0] !== "ujsvqBWRMnqdwPx" &&
material[0] !== "hUlRcbieVuIiOXG" &&
material[0] !== "jlzuBkUzuJqgiAK" &&
material[0] !== "xNrofRCqOXXHVZt"
) {
material[1].color = new THREE.Color(props.item.color[0]);
}
material[1].needsUpdate = true;
});
}, [materials, props.item]);
VideoCarousel.jsx
```javascript import gsap from "gsap"; import { useGSAP } from "@gsap/react"; import { ScrollTrigger } from "gsap/all"; gsap.registerPlugin(ScrollTrigger); import { useEffect, useRef, useState } from "react";
import { hightlightsSlides } from "../constants"; import { pauseImg, playImg, replayImg } from "../utils";
const VideoCarousel = () => { const videoRef = useRef([]); const videoSpanRef = useRef([]); const videoDivRef = useRef([]);
// video and indicator const [video, setVideo] = useState({ isEnd: false, startPlay: false, videoId: 0, isLastVideo: false, isPlaying: false, });
const [loadedData, setLoadedData] = useState([]); const { isEnd, isLastVideo, startPlay, videoId, isPlaying } = video;
useGSAP(() => {
// slider animation to move the video out of the screen and bring the next video in
gsap.to("#slider", {
transform: translateX(${-100 * videoId}%),
duration: 2,
ease: "power2.inOut", // show visualizer https://gsap.com/docs/v3/Eases
});
// video animation to play the video when it is in the view
gsap.to("#video", {
scrollTrigger: {
trigger: "#video",
toggleActions: "restart none none none",
},
onComplete: () => {
setVideo((pre) => ({
...pre,
startPlay: true,
isPlaying: true,
}));
},
});
}, [isEnd, videoId]);
useEffect(() => { let currentProgress = 0; let span = videoSpanRef.current;
if (span[videoId]) {
// animation to move the indicator
let anim = gsap.to(span[videoId], {
onUpdate: () => {
// get the progress of the video
const progress = Math.ceil(anim.progress() * 100);
if (progress != currentProgress) {
currentProgress = progress;
// set the width of the progress bar
gsap.to(videoDivRef.current[videoId], {
width:
window.innerWidth < 760
? "10vw" // mobile
: window.innerWidth < 1200
? "10vw" // tablet
: "4vw", // laptop
});
// set the background color of the progress bar
gsap.to(span[videoId], {
width: `${currentProgress}%`,
backgroundColor: "white",
});
}
},
// when the video is ended, replace the progress bar with the indicator and change the background color
onComplete: () => {
if (isPlaying) {
gsap.to(videoDivRef.current[videoId], {
width: "12px",
});
gsap.to(span[videoId], {
backgroundColor: "#afafaf",
});
}
},
});
if (videoId == 0) {
anim.restart();
}
// update the progress bar
const animUpdate = () => {
anim.progress(
videoRef.current[videoId].currentTime /
hightlightsSlides[videoId].videoDuration
);
};
if (isPlaying) {
// ticker to update the progress bar
gsap.ticker.add(animUpdate);
} else {
// remove the ticker when the video is paused (progress bar is stopped)
gsap.ticker.remove(animUpdate);
}
}
}, [videoId, startPlay]);
useEffect(() => { if (loadedData.length > 3) { if (!isPlaying) { videoRef.current[videoId].pause(); } else { startPlay && videoRef.current[videoId].play(); } } }, [startPlay, videoId, isPlaying, loadedData]);
// vd id is the id for every video until id becomes number 3 const handleProcess = (type, i) => { switch (type) { case "video-end": setVideo((pre) => ({ ...pre, isEnd: true, videoId: i + 1 })); break;
case "video-last":
setVideo((pre) => ({ ...pre, isLastVideo: true }));
break;
case "video-reset":
setVideo((pre) => ({ ...pre, videoId: 0, isLastVideo: false }));
break;
case "pause":
setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying }));
break;
case "play":
setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying }));
break;
default:
return video;
}
};
const handleLoadedMetaData = (i, e) => setLoadedData((pre) => [...pre, e]);
return ( <>
{hightlightsSlides.map((list, i) => (
<div cla
$ claude mcp add iphone \
-- python -m otcore.mcp_server <graph>