├── .eslintrc.cjs
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── assets
│ ├── images
│ │ ├── apple.svg
│ │ ├── bag.svg
│ │ ├── black.jpg
│ │ ├── blue.jpg
│ │ ├── chip.jpeg
│ │ ├── explore1.jpg
│ │ ├── explore2.jpg
│ │ ├── frame.png
│ │ ├── hero.jpeg
│ │ ├── pause.svg
│ │ ├── play.svg
│ │ ├── replay.svg
│ │ ├── right.svg
│ │ ├── search.svg
│ │ ├── watch.svg
│ │ ├── white.jpg
│ │ └── yellow.jpg
│ ├── react.svg
│ └── videos
│ │ ├── explore.mp4
│ │ ├── frame.mp4
│ │ ├── hero.mp4
│ │ ├── highlight-first.mp4
│ │ ├── hightlight-fourth.mp4
│ │ ├── hightlight-sec.mp4
│ │ ├── hightlight-third.mp4
│ │ └── smallHero.mp4
└── models
│ └── scene.glb
├── src
├── App.css
├── App.jsx
├── components
│ ├── Features.jsx
│ ├── Footer.jsx
│ ├── Hero.jsx
│ ├── Highlights.jsx
│ ├── HowItWorks.jsx
│ ├── IPhone.jsx
│ ├── Lights.jsx
│ ├── Loader.jsx
│ ├── Model.jsx
│ ├── ModelView.jsx
│ ├── Navbar.jsx
│ └── VideoCarousel.jsx
├── constants
│ └── index.js
├── index.css
├── main.jsx
└── utils
│ ├── animations.js
│ └── index.js
├── tailwind.config.js
└── vite.config.js
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh','@react-three'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 | # T h r e e . j s
10 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Apple Web
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apple_web",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@gsap/react": "^2.1.1",
14 | "@react-three/drei": "^9.107.0",
15 | "@react-three/fiber": "^8.16.8",
16 | "gsap": "^3.12.5",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "three": "^0.165.0"
20 | },
21 | "devDependencies": {
22 | "@types/react": "^18.2.66",
23 | "@types/react-dom": "^18.2.22",
24 | "@vitejs/plugin-react": "^4.2.1",
25 | "autoprefixer": "^10.4.19",
26 | "eslint": "^8.57.0",
27 | "eslint-plugin-react": "^7.34.1",
28 | "eslint-plugin-react-hooks": "^4.6.0",
29 | "eslint-plugin-react-refresh": "^0.4.6",
30 | "postcss": "^8.4.38",
31 | "tailwindcss": "^3.4.4",
32 | "vite": "^5.3.2"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/images/apple.svg:
--------------------------------------------------------------------------------
1 |
8 |
13 |
--------------------------------------------------------------------------------
/public/assets/images/bag.svg:
--------------------------------------------------------------------------------
1 |
9 |
14 |
--------------------------------------------------------------------------------
/public/assets/images/black.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/black.jpg
--------------------------------------------------------------------------------
/public/assets/images/blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/blue.jpg
--------------------------------------------------------------------------------
/public/assets/images/chip.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/chip.jpeg
--------------------------------------------------------------------------------
/public/assets/images/explore1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/explore1.jpg
--------------------------------------------------------------------------------
/public/assets/images/explore2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/explore2.jpg
--------------------------------------------------------------------------------
/public/assets/images/frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/frame.png
--------------------------------------------------------------------------------
/public/assets/images/hero.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/hero.jpeg
--------------------------------------------------------------------------------
/public/assets/images/pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/images/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/images/replay.svg:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/assets/images/right.svg:
--------------------------------------------------------------------------------
1 |
9 |
13 |
--------------------------------------------------------------------------------
/public/assets/images/search.svg:
--------------------------------------------------------------------------------
1 |
9 |
14 |
--------------------------------------------------------------------------------
/public/assets/images/watch.svg:
--------------------------------------------------------------------------------
1 |
9 |
13 |
--------------------------------------------------------------------------------
/public/assets/images/white.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/white.jpg
--------------------------------------------------------------------------------
/public/assets/images/yellow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/yellow.jpg
--------------------------------------------------------------------------------
/public/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/videos/explore.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/explore.mp4
--------------------------------------------------------------------------------
/public/assets/videos/frame.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/frame.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hero.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hero.mp4
--------------------------------------------------------------------------------
/public/assets/videos/highlight-first.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/highlight-first.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hightlight-fourth.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hightlight-fourth.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hightlight-sec.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hightlight-sec.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hightlight-third.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hightlight-third.mp4
--------------------------------------------------------------------------------
/public/assets/videos/smallHero.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/smallHero.mp4
--------------------------------------------------------------------------------
/public/models/scene.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/models/scene.glb
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Navbar from "./components/Navbar"
2 | import Hero from "./components/Hero"
3 | import Highlights from "./components/Highlights"
4 | import Model from "./components/Model"
5 | import Features from "./components/Features"
6 | import HowItWorks from "./components/HowItWorks"
7 | import Footer from "./components/Footer"
8 |
9 | const App = () => {
10 |
11 | return (
12 |
13 | < Navbar/>
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default App
25 |
--------------------------------------------------------------------------------
/src/components/Features.jsx:
--------------------------------------------------------------------------------
1 | import { useGSAP } from '@gsap/react'
2 | import React, { useRef } from 'react'
3 | import { animateWithGsap } from "../utils/animations"
4 | import { explore1Img, explore2Img, exploreVideo } from "../utils"
5 | import gsap from 'gsap'
6 | const Features = () => {
7 |
8 | const videoRef = useRef();
9 |
10 | useGSAP(() => {
11 |
12 | gsap.to('#exploreVideo', {
13 |
14 | scrollTrigger: {
15 | trigger: '#exploreVideo',
16 | toggleActions: 'play pause reverse restart ',
17 | start: '-10% bottom',
18 | },
19 | onComplete: () => {
20 | videoRef.current.play();
21 | }
22 | })
23 |
24 | animateWithGsap('#features_title', {
25 | opacity: 1,
26 | y: 0,
27 |
28 | })
29 |
30 | animateWithGsap('.g_grow', {
31 | scale: 1,
32 | opacity: 1,
33 | ease: 'power1.inOut',
34 | },
35 | { scrub: 5.5 }
36 | )
37 |
38 | animateWithGsap('.g_text', {
39 | y: 0,
40 | opacity: 1,
41 | ease: 'power1.inOut',
42 | duration: 1}
43 |
44 | )
45 |
46 | },[]);
47 |
48 | return (
49 |
51 |
52 |
53 |
54 | Explore the full story.
55 |
56 |
57 |
58 |
60 |
61 |
iPhone
62 |
63 |
64 | Forged In Titanium
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
81 |
82 |
83 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | iPhone 15 Pro is {''}
92 |
93 | the first iPhone to feature an aerospace-grade titanium design
94 | ,
95 | using the same alloy that spacecrafts use for missions to Mars.
96 |
97 |
98 |
99 |
100 | Titanium has one of the best strength-to-weight ratios of any metal, making these our {' '}
101 |
102 | lightest Pro models ever.
103 |
104 | You'll notice the difference the moment you pick one up.
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | )
117 | }
118 |
119 | export default Features
120 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { footerLinks } from '../constants'
3 |
4 | const Footer = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | More ways to shop:
13 |
14 | Find an Apple Store {' '}
15 |
16 | or{' '}
17 |
18 | other retailer
19 | {' '}
20 | near you.
21 |
22 |
23 | Or call 000800-040-1966
24 |
25 |
26 |
27 |
28 |
Copright @ 2024 Apple Inc. All rights reserved.
29 |
30 | {footerLinks.map((link, i) => (
31 |
32 | {link}{' '}
33 | {i !== footerLinks.length - 1 && (
34 | |
35 | )}
36 |
37 | ))}
38 |
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | export default Footer
48 |
--------------------------------------------------------------------------------
/src/components/Hero.jsx:
--------------------------------------------------------------------------------
1 | import gsap from "gsap"
2 | import { useGSAP } from "@gsap/react"
3 | import { heroVideo, smallHeroVideo } from "../utils"
4 | import { useEffect, useState } from "react"
5 | const Hero = () => {
6 | const [videoSrc, setVideoSrc] = useState(window.innerWidth < 760 ? smallHeroVideo : heroVideo)
7 |
8 | const handleVideoSrcSet = () => {
9 | if(window.innerWidth < 760) {
10 | setVideoSrc(smallHeroVideo)
11 | } else {
12 | setVideoSrc(heroVideo)
13 | }
14 | }
15 |
16 | useEffect(() => {
17 | window.addEventListener('resize', handleVideoSrcSet);
18 |
19 | return () => {
20 | window.removeEventListener('reisze', handleVideoSrcSet)
21 | }
22 | }, [])
23 |
24 | useGSAP(() => {
25 | gsap.to('#hero', { opacity: 1, delay: 2 })
26 | gsap.to('#cta', { opacity: 1, y: -50, delay: 2 })
27 | }, [])
28 |
29 | return (
30 |
31 |
32 |
iPhone 15 Pro
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
Buy
45 |
From $199/month or $999
46 |
47 |
48 | )
49 | }
50 |
51 | export default Hero
52 |
--------------------------------------------------------------------------------
/src/components/Highlights.jsx:
--------------------------------------------------------------------------------
1 | import { useGSAP } from "@gsap/react"
2 | import gsap from "gsap"
3 | import { rightImg, watchImg } from "../utils"
4 | import VideoCarousel from "./VideoCarousel"
5 |
6 | const Highlights = () => {
7 |
8 | useGSAP(() => {
9 | gsap.to("#title", {
10 | opacity: 1,
11 | y: 0,
12 | duration: 1.5,
13 | rotate:360,
14 | ease: "power1.out",
15 | })
16 |
17 | gsap.to(".link", {
18 | opacity: 1,
19 | y: 0,
20 | duration: 1.5,
21 | stagger: 0.25,
22 |
23 | })
24 | }, [])
25 | return (
26 |
28 |
29 |
30 |
31 |
32 |
33 | Get the highlights.
34 |
35 |
36 |
37 |
38 |
39 |
Watch the film
40 |
41 |
42 |
Watch the event
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default Highlights
58 |
--------------------------------------------------------------------------------
/src/components/HowItWorks.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import { chipImg, frameImg, frameVideo } from '../utils'
3 | import { useGSAP } from '@gsap/react'
4 | import gsap from 'gsap'
5 | import { animateWithGsap } from '../utils/animations'
6 |
7 | const HowItWorks = () => {
8 |
9 | const videoRef = useRef();
10 |
11 | useGSAP(() => {
12 |
13 | gsap.from('#chip', {
14 | scrollTrigger: {
15 | trigger: '#chip',
16 | start: 'top 80%',
17 | },
18 | opacity: 0,
19 | scale: 2,
20 | duration: 2,
21 | ease: 'power1.inOut',
22 | })
23 |
24 | animateWithGsap('.g_fadeIn', {
25 | opacity: 1,
26 | y: 0,
27 | duration: 1,
28 | ease: 'power2.inOut'
29 | })
30 |
31 | }, [])
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | A17 Pro chip.
42 | A monster win for gaming.
43 |
44 |
45 |
46 | It's here. The biggest redesign in the history of Apple GPUs.
47 |
48 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
Honkai: Star Rail
66 |
67 |
68 |
69 |
70 |
71 | A17 Pro is an entirely new class of iPhone chip that delivers our {' '}
72 |
73 | best graphic performance by far
74 | .
75 |
76 |
77 |
78 | Mobile {' '}
79 |
80 | games will look and feel so immersive
81 | ,
82 | with incredibly detailed environments and characters.
83 |
84 |
85 |
86 |
87 |
88 |
New
89 |
Pro-class GPU
90 |
with 6 cores
91 |
92 |
93 |
94 |
95 | )
96 | }
97 |
98 | export default HowItWorks
99 |
--------------------------------------------------------------------------------
/src/components/IPhone.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | Auto-generated by: https://github.com/pmndrs/gltfjsx
3 | Author: polyman (https://sketchfab.com/Polyman_3D)
4 | License: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
5 | Source: https://sketchfab.com/3d-models/apple-iphone-15-pro-max-black-df17520841214c1792fb8a44c6783ee7
6 | Title: Apple iPhone 15 Pro Max Black
7 | */
8 |
9 | import * as THREE from 'three';
10 | import React, { useEffect, useRef } from "react";
11 | import { useGLTF, useTexture } from "@react-three/drei";
12 |
13 | function Model(props) {
14 | const { nodes, materials } = useGLTF("/models/scene.glb");
15 |
16 | const texture = useTexture(props.item.img);
17 |
18 | useEffect(() => {
19 | Object.entries(materials).map((material) => {
20 | // these are the material names that can't be changed color
21 | if (
22 | material[0] !== "zFdeDaGNRwzccye" &&
23 | material[0] !== "ujsvqBWRMnqdwPx" &&
24 | material[0] !== "hUlRcbieVuIiOXG" &&
25 | material[0] !== "jlzuBkUzuJqgiAK" &&
26 | material[0] !== "xNrofRCqOXXHVZt"
27 | ) {
28 | material[1].color = new THREE.Color(props.item.color[0]);
29 | }
30 | material[1].needsUpdate = true;
31 | });
32 | }, [materials, props.item]);
33 |
34 | return (
35 |
36 |
43 |
50 |
57 |
64 |
71 |
78 |
85 |
92 |
99 |
106 |
113 |
120 |
127 |
134 |
141 |
148 |
149 |
150 |
157 |
164 |
171 |
178 |
185 |
192 |
199 |
206 |
213 |
220 |
227 |
234 |
241 |
248 |
255 |
256 | );
257 | }
258 |
259 | export default Model;
260 |
261 | useGLTF.preload("/models/scene.glb");
--------------------------------------------------------------------------------
/src/components/Lights.jsx:
--------------------------------------------------------------------------------
1 | import { Environment, Lightformer } from "@react-three/drei";
2 |
3 | const Lights = () => {
4 | return (
5 | // group different lights and lightformers. We can use group to organize lights, cameras, meshes, and other objects in the scene.
6 |
7 | {/**
8 | * @description Environment is used to create a background environment for the scene
9 | * https://github.com/pmndrs/drei?tab=readme-ov-file#environment
10 | */}
11 |
12 |
13 | {/**
14 | * @description Lightformer used to create custom lights with various shapes and properties in a 3D scene.
15 | * https://github.com/pmndrs/drei?tab=readme-ov-file#lightformer
16 | */}
17 |
24 |
31 |
38 |
39 |
40 |
41 | {/**
42 | * @description spotLight is used to create a light source positioned at a specific point
43 | * in the scene that emits light in a specific direction.
44 | * https://threejs.org/docs/#api/en/lights/SpotLight
45 | */}
46 |
54 |
62 |
69 |
70 | );
71 | };
72 |
73 | export default Lights;
--------------------------------------------------------------------------------
/src/components/Loader.jsx:
--------------------------------------------------------------------------------
1 | import { Html } from '@react-three/drei'
2 | import React from 'react'
3 |
4 | const Loader = () => {
5 | return (
6 |
7 |
8 |
9 | Loading...
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default Loader
--------------------------------------------------------------------------------
/src/components/Model.jsx:
--------------------------------------------------------------------------------
1 | import { useGSAP } from "@gsap/react"
2 | import gsap from "gsap";
3 | import ModelView from "./ModelView";
4 | import { useEffect, useRef, useState } from "react";
5 | import { yellowImg } from "../utils";
6 |
7 | import * as THREE from 'three';
8 | import { Canvas } from "@react-three/fiber";
9 | import { View } from "@react-three/drei";
10 | import { models, sizes } from "../constants";
11 | import { animateWithGsapTimeline } from "../utils/animations";
12 |
13 | const Model = () => {
14 | const [size, setSize] = useState('small');
15 | const [model, setModel] = useState({
16 | title: 'iPhone 15 Pro in Natural Titanium',
17 | color: ['#8F8A81', '#FFE7B9', '#6F6C64'],
18 | img: yellowImg,
19 | })
20 |
21 | // camera control for the model view
22 | const cameraControlSmall = useRef();
23 | const cameraControlLarge = useRef();
24 |
25 | // model
26 | const small = useRef(new THREE.Group());
27 | const large = useRef(new THREE.Group());
28 |
29 | // rotation
30 | const [smallRotation, setSmallRotation] = useState(0);
31 | const [largeRotation, setLargeRotation] = useState(0);
32 |
33 | const tl = gsap.timeline();
34 |
35 | useEffect(() => {
36 | if(size === 'large') {
37 | animateWithGsapTimeline(tl, small, smallRotation, '#view1', '#view2', {
38 | transform: 'translateX(-100%)',
39 | duration: 2
40 | })
41 | }
42 |
43 | if(size ==='small') {
44 | animateWithGsapTimeline(tl, large, largeRotation, '#view2', '#view1', {
45 | transform: 'translateX(0)',
46 | duration: 2
47 | })
48 | }
49 | }, [size])
50 |
51 | useGSAP(() => {
52 | gsap.to('#heading', { y: 0, opacity: 1 })
53 | }, []);
54 |
55 | return (
56 |
57 |
58 |
59 | Take a closer look.
60 |
61 |
62 |
63 |
64 |
73 |
74 |
83 |
84 |
96 |
97 |
98 |
99 |
100 |
101 |
{model.title}
102 |
103 |
104 |
105 | {models.map((item, i) => (
106 | setModel(item)} />
107 | ))}
108 |
109 |
110 |
111 | {sizes.map(({ label, value }) => (
112 | setSize(value)}>
113 | {label}
114 |
115 | ))}
116 |
117 |
118 |
119 |
120 |
121 |
122 | )
123 | }
124 |
125 | export default Model
--------------------------------------------------------------------------------
/src/components/ModelView.jsx:
--------------------------------------------------------------------------------
1 | import { Html, OrbitControls, PerspectiveCamera, View } from "@react-three/drei"
2 |
3 | import * as THREE from 'three'
4 | import Lights from './Lights';
5 | import Loader from './Loader';
6 | import IPhone from './IPhone';
7 | import { Suspense } from "react";
8 |
9 | const ModelView = ({ index, groupRef, gsapType, controlRef, setRotationState, size, item }) => {
10 | return (
11 |
16 | {/* Ambient Light */}
17 |
18 |
19 |
20 |
21 |
22 |
23 | setRotationState(controlRef.current.getAzimuthalAngle())}
31 | />
32 |
33 |
34 | }>
35 |
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default ModelView
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { navLists } from "../constants"
2 | import { appleImg, bagImg, searchImg } from "../utils"
3 | const Navbar = () => {
4 | return (
5 |
7 |
8 |
10 |
11 |
13 | {navLists.map((nav) => (
14 |
16 | {nav}
17 |
18 | ))}
19 |
20 |
21 |
23 |
25 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default Navbar
35 |
--------------------------------------------------------------------------------
/src/components/VideoCarousel.jsx:
--------------------------------------------------------------------------------
1 | import gsap from "gsap";
2 | import { useGSAP } from "@gsap/react";
3 | import { ScrollTrigger } from "gsap/all";
4 | gsap.registerPlugin(ScrollTrigger);
5 | import { useEffect, useRef, useState } from "react";
6 |
7 | import { hightlightsSlides } from "../constants";
8 | import { pauseImg, playImg, replayImg } from "../utils";
9 |
10 | const VideoCarousel = () => {
11 | const videoRef = useRef([]);
12 | const videoSpanRef = useRef([]);
13 | const videoDivRef = useRef([]);
14 |
15 | // video and indicator
16 | const [video, setVideo] = useState({
17 | isEnd: false,
18 | startPlay: false,
19 | videoId: 0,
20 | isLastVideo: false,
21 | isPlaying: false,
22 | });
23 |
24 | const [loadedData, setLoadedData] = useState([]);
25 | const { isEnd, isLastVideo, startPlay, videoId, isPlaying } = video;
26 |
27 | useGSAP(() => {
28 | // slider animation to move the video out of the screen and bring the next video in
29 | gsap.to("#slider", {
30 | transform: `translateX(${-100 * videoId}%)`,
31 | duration: 2,
32 | ease: "power2.inOut", // show visualizer https://gsap.com/docs/v3/Eases
33 | });
34 |
35 | // video animation to play the video when it is in the view
36 | gsap.to("#video", {
37 | scrollTrigger: {
38 | trigger: "#video",
39 | toggleActions: "restart none none none",
40 | },
41 | onComplete: () => {
42 | setVideo((pre) => ({
43 | ...pre,
44 | startPlay: true,
45 | isPlaying: true,
46 | }));
47 | },
48 | });
49 | }, [isEnd, videoId]);
50 |
51 | useEffect(() => {
52 | let currentProgress = 0;
53 | let span = videoSpanRef.current;
54 |
55 | if (span[videoId]) {
56 | // animation to move the indicator
57 | let anim = gsap.to(span[videoId], {
58 | onUpdate: () => {
59 | // get the progress of the video
60 | const progress = Math.ceil(anim.progress() * 100);
61 |
62 | if (progress != currentProgress) {
63 | currentProgress = progress;
64 |
65 | // set the width of the progress bar
66 | gsap.to(videoDivRef.current[videoId], {
67 | width:
68 | window.innerWidth < 760
69 | ? "10vw" // mobile
70 | : window.innerWidth < 1200
71 | ? "10vw" // tablet
72 | : "4vw", // laptop
73 | });
74 |
75 | // set the background color of the progress bar
76 | gsap.to(span[videoId], {
77 | width: `${currentProgress}%`,
78 | backgroundColor: "white",
79 | });
80 | }
81 | },
82 |
83 | // when the video is ended, replace the progress bar with the indicator and change the background color
84 | onComplete: () => {
85 | if (isPlaying) {
86 | gsap.to(videoDivRef.current[videoId], {
87 | width: "12px",
88 | });
89 | gsap.to(span[videoId], {
90 | backgroundColor: "#afafaf",
91 | });
92 | }
93 | },
94 | });
95 |
96 | if (videoId == 0) {
97 | anim.restart();
98 | }
99 |
100 | // update the progress bar
101 | const animUpdate = () => {
102 | anim.progress(
103 | videoRef.current[videoId].currentTime /
104 | hightlightsSlides[videoId].videoDuration
105 | );
106 | };
107 |
108 | if (isPlaying) {
109 | // ticker to update the progress bar
110 | gsap.ticker.add(animUpdate);
111 | } else {
112 | // remove the ticker when the video is paused (progress bar is stopped)
113 | gsap.ticker.remove(animUpdate);
114 | }
115 | }
116 | }, [videoId, startPlay]);
117 |
118 | useEffect(() => {
119 | if (loadedData.length > 3) {
120 | if (!isPlaying) {
121 | videoRef.current[videoId].pause();
122 | } else {
123 | startPlay && videoRef.current[videoId].play();
124 | }
125 | }
126 | }, [startPlay, videoId, isPlaying, loadedData]);
127 |
128 | // vd id is the id for every video until id becomes number 3
129 | const handleProcess = (type, i) => {
130 | switch (type) {
131 | case "video-end":
132 | setVideo((pre) => ({ ...pre, isEnd: true, videoId: i + 1 }));
133 | break;
134 |
135 | case "video-last":
136 | setVideo((pre) => ({ ...pre, isLastVideo: true }));
137 | break;
138 |
139 | case "video-reset":
140 | setVideo((pre) => ({ ...pre, videoId: 0, isLastVideo: false }));
141 | break;
142 |
143 | case "pause":
144 | setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying }));
145 | break;
146 |
147 | case "play":
148 | setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying }));
149 | break;
150 |
151 | default:
152 | return video;
153 | }
154 | };
155 |
156 | const handleLoadedMetaData = (i, e) => setLoadedData((pre) => [...pre, e]);
157 |
158 | return (
159 | <>
160 |
161 | {hightlightsSlides.map((list, i) => (
162 |
163 |
164 |
165 | (videoRef.current[i] = el)}
174 | onEnded={() =>
175 | i !== 3
176 | ? handleProcess("video-end", i)
177 | : handleProcess("video-last")
178 | }
179 | onPlay={() =>
180 | setVideo((pre) => ({ ...pre, isPlaying: true }))
181 | }
182 | onLoadedMetadata={(e) => handleLoadedMetaData(i, e)}
183 | >
184 |
185 |
186 |
187 |
188 |
189 | {list.textLists.map((text, i) => (
190 |
191 | {text}
192 |
193 | ))}
194 |
195 |
196 |
197 | ))}
198 |
199 |
200 |
201 |
202 | {videoRef.current.map((_, i) => (
203 | (videoDivRef.current[i] = el)}
207 | >
208 | (videoSpanRef.current[i] = el)}
211 | />
212 |
213 | ))}
214 |
215 |
216 |
217 | handleProcess("video-reset")
223 | : !isPlaying
224 | ? () => handleProcess("play")
225 | : () => handleProcess("pause")
226 | }
227 | />
228 |
229 |
230 | >
231 | );
232 | };
233 |
234 | export default VideoCarousel;
235 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | blackImg,
3 | blueImg,
4 | highlightFirstVideo,
5 | highlightFourthVideo,
6 | highlightSecondVideo,
7 | highlightThirdVideo,
8 | whiteImg,
9 | yellowImg,
10 | } from "../utils";
11 |
12 | export const navLists = ["Store", "Mac", "iPhone", "Support"];
13 |
14 | export const hightlightsSlides = [
15 | {
16 | id: 1,
17 | textLists: [
18 | "Enter A17 Pro.",
19 | "Game‑changing chip.",
20 | "Groundbreaking performance.",
21 | ],
22 | video: highlightFirstVideo,
23 | videoDuration: 4,
24 | },
25 | {
26 | id: 2,
27 | textLists: ["Titanium.", "So strong. So light. So Pro."],
28 | video: highlightSecondVideo,
29 | videoDuration: 5,
30 | },
31 | {
32 | id: 3,
33 | textLists: [
34 | "iPhone 15 Pro Max has the",
35 | "longest optical zoom in",
36 | "iPhone ever. Far out.",
37 | ],
38 | video: highlightThirdVideo,
39 | videoDuration: 2,
40 | },
41 | {
42 | id: 4,
43 | textLists: ["All-new Action button.", "What will yours do?."],
44 | video: highlightFourthVideo,
45 | videoDuration: 3.63,
46 | },
47 | ];
48 |
49 | export const models = [
50 | {
51 | id: 1,
52 | title: "iPhone 15 Pro in Natural Titanium",
53 | color: ["#8F8A81", "#ffe7b9", "#6f6c64"],
54 | img: yellowImg,
55 | },
56 | {
57 | id: 2,
58 | title: "iPhone 15 Pro in Blue Titanium",
59 | color: ["#53596E", "#6395ff", "#21242e"],
60 | img: blueImg,
61 | },
62 | {
63 | id: 3,
64 | title: "iPhone 15 Pro in White Titanium",
65 | color: ["#C9C8C2", "#ffffff", "#C9C8C2"],
66 | img: whiteImg,
67 | },
68 | {
69 | id: 4,
70 | title: "iPhone 15 Pro in Black Titanium",
71 | color: ["#454749", "#3b3b3b", "#181819"],
72 | img: blackImg,
73 | },
74 | ];
75 |
76 | export const sizes = [
77 | { label: '6.1"', value: "small" },
78 | { label: '6.7"', value: "large" },
79 | ];
80 |
81 | export const footerLinks = [
82 | "Privacy Policy",
83 | "Terms of Use",
84 | "Sales Policy",
85 | "Legal",
86 | "Site Map",
87 | ];
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | body {
12 | color: white;
13 | width: 100dvw;
14 | overflow-x: hidden;
15 | height: 100%;
16 | background: #000;
17 | border-color: #3b3b3b;
18 | user-select: none;
19 | }
20 |
21 | canvas {
22 | touch-action: none;
23 | }
24 |
25 | .screen-max-width {
26 | margin-inline-start: auto;
27 | margin-inline-end: auto;
28 | position: relative;
29 | max-width: 1120px;
30 | }
31 |
32 | @layer utilities {
33 | .flex-center {
34 | @apply flex items-center justify-center
35 | }
36 |
37 | .nav-height {
38 | @apply h-[calc(100vh-60px)]
39 | }
40 |
41 | .btn {
42 | @apply px-5 py-2 rounded-3xl bg-blue my-5 hover:bg-transparent border border-transparent hover:border hover:text-blue hover:border-blue
43 | }
44 |
45 | .color-container {
46 | @apply flex items-center justify-center px-4 py-4 rounded-full bg-gray-300 backdrop-blur
47 | }
48 |
49 | .size-btn-container {
50 | @apply flex items-center justify-center p-1 rounded-full bg-gray-300 backdrop-blur ml-3 gap-1
51 | }
52 |
53 | .size-btn {
54 | @apply w-10 h-10 text-sm flex justify-center items-center bg-white text-black rounded-full transition-all
55 | }
56 |
57 | .common-padding {
58 | @apply sm:py-32 py-20 sm:px-10 px-5
59 | }
60 |
61 | .section-heading {
62 | @apply text-gray lg:text-6xl md:text-5xl text-3xl lg:mb-0 mb-5 font-medium opacity-0 translate-y-20
63 | }
64 |
65 | .feature-text {
66 | @apply text-gray max-w-md text-lg md:text-xl font-semibold opacity-0 translate-y-[100px]
67 | }
68 |
69 | .feature-text-container {
70 | @apply w-full flex-center flex-col md:flex-row mt-10 md:mt-16 gap-5
71 | }
72 |
73 | .feature-video {
74 | @apply w-full h-full object-cover object-center scale-150 opacity-0
75 | }
76 |
77 | .feature-video-container {
78 | @apply w-full flex flex-col md:flex-row gap-5 items-center
79 | }
80 |
81 | .link {
82 | @apply text-blue hover:underline cursor-pointer flex items-center text-xl opacity-0 translate-y-20
83 | }
84 |
85 | .control-btn {
86 | @apply ml-4 p-4 rounded-full bg-gray-300 backdrop-blur flex-center
87 | }
88 |
89 | .hero-title {
90 | @apply text-center font-semibold text-3xl text-gray-100 opacity-0 max-md:mb-10
91 | }
92 |
93 | .hiw-title {
94 | @apply text-4xl md:text-7xl font-semibold text-center
95 | }
96 |
97 | .hiw-subtitle {
98 | @apply text-gray font-semibold text-xl md:text-2xl py-10 text-center
99 | }
100 |
101 | .hiw-video {
102 | @apply absolute w-[95%] h-[90%] rounded-[56px] overflow-hidden
103 | }
104 |
105 | .hiw-text-container {
106 | @apply flex md:flex-row flex-col justify-between items-start gap-24
107 | }
108 |
109 | .hiw-text {
110 | @apply text-gray text-xl font-normal md:font-semibold
111 | }
112 |
113 | .hiw-bigtext {
114 | @apply text-white text-3xl md:text-5xl font-normal md:font-semibold my-2
115 | }
116 |
117 | .video-carousel_container {
118 | @apply relative sm:w-[70vw] w-[88vw] md:h-[70vh] sm:h-[50vh] h-[35vh]
119 | }
120 |
121 | .g_fadeIn {
122 | @apply opacity-0 translate-y-[100px]
123 | }
124 | }
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/src/utils/animations.js:
--------------------------------------------------------------------------------
1 | import gsap from "gsap"
2 |
3 | import { ScrollTrigger } from "gsap/all"
4 | gsap.registerPlugin(ScrollTrigger);
5 |
6 | export const animateWithGsap = (target, animationProps, scrollProps) => {
7 | gsap.to(target, {
8 | ...animationProps,
9 | scrollTrigger: {
10 | trigger: target,
11 | toggleActions: 'restart reverse restart reverse',
12 | start: 'top 85%',
13 | ...scrollProps,
14 | }
15 | })
16 | }
17 |
18 | export const animateWithGsapTimeline = (timeline, rotationRef, rotationState, firstTarget, secondTarget, animationProps) => {
19 | timeline.to(rotationRef.current.rotation, {
20 | y: rotationState,
21 | duration: 1,
22 | ease: 'power2.inOut'
23 | })
24 |
25 | timeline.to(
26 | firstTarget,
27 | {
28 | ...animationProps,
29 | ease: 'power2.inOut'
30 | },
31 | '<'
32 | )
33 |
34 | timeline.to(
35 | secondTarget,
36 | {
37 | ...animationProps,
38 | ease: 'power2.inOut'
39 | },
40 | '<'
41 | )
42 | }
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import hero from "/assets/images/hero.jpeg";
2 |
3 | export const heroImg = hero;
4 |
5 | import hmv from "/assets/videos/hero.mp4";
6 | import smallmv from "/assets/videos/smallHero.mp4";
7 | import highlightFirstmv from "/assets/videos/highlight-first.mp4";
8 | import highlightSectmv from "/assets/videos/hightlight-third.mp4";
9 | import highlightThirdmv from "/assets/videos/hightlight-sec.mp4";
10 | import highlightFourthmv from "/assets/videos/hightlight-fourth.mp4";
11 | import exploremv from "/assets/videos/explore.mp4";
12 | import framemv from "/assets/videos/frame.mp4";
13 |
14 | import apple from "/assets/images/apple.svg";
15 | import search from "/assets/images/search.svg";
16 | import bag from "/assets/images/bag.svg";
17 | import watch from "/assets/images/watch.svg";
18 | import right from "/assets/images/right.svg";
19 | import replay from "/assets/images/replay.svg";
20 | import play from "/assets/images/play.svg";
21 | import pause from "/assets/images/pause.svg";
22 |
23 | import yellow from "/assets/images/yellow.jpg";
24 | import blue from "/assets/images/blue.jpg";
25 | import white from "/assets/images/white.jpg";
26 | import black from "/assets/images/black.jpg";
27 | import explore1 from "/assets/images/explore1.jpg";
28 | import explore2 from "/assets/images/explore2.jpg";
29 | import chip from "/assets/images/chip.jpeg";
30 | import frame from "/assets/images/frame.png";
31 |
32 | export const heroVideo = hmv;
33 | export const smallHeroVideo = smallmv;
34 | export const highlightFirstVideo = highlightFirstmv;
35 | export const highlightSecondVideo = highlightSectmv;
36 | export const highlightThirdVideo = highlightThirdmv;
37 | export const highlightFourthVideo = highlightFourthmv;
38 | export const exploreVideo = exploremv;
39 | export const frameVideo = framemv;
40 |
41 | export const appleImg = apple;
42 | export const searchImg = search;
43 | export const bagImg = bag;
44 | export const watchImg = watch;
45 | export const rightImg = right;
46 | export const replayImg = replay;
47 | export const playImg = play;
48 | export const pauseImg = pause;
49 |
50 | export const yellowImg = yellow;
51 | export const blueImg = blue;
52 | export const whiteImg = white;
53 | export const blackImg = black;
54 | export const explore1Img = explore1;
55 | export const explore2Img = explore2;
56 | export const chipImg = chip;
57 | export const frameImg = frame;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | colors: {
10 | blue: "#2997FF",
11 | gray: {
12 | DEFAULT: "#86868b",
13 | 100: "#94928d",
14 | 200: "#afafaf",
15 | 300: "#42424570",
16 | },
17 | zinc: "#101010",
18 | }
19 | },
20 | },
21 | plugins: [],
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------