├── .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
└── vite.svg
├── 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'],
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 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
Iphone 15 Website
15 |
16 |
17 |
18 | ## 📋 Table of Contents
19 |
20 | 1. 🤖 [Introduction](#introduction)
21 | 2. ⚙️ [Tech Stack](#tech-stack)
22 | 3. 🔋 [Features](#features)
23 | 4. 🤸 [Quick Start](#quick-start)
24 | 5. 🕸️ [Snippets](#snippets)
25 | 6. 🔗 [Links](#links)
26 | 7. 🚀 [More](#more)
27 |
28 | ## 🤖 Introduction
29 |
30 | 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.
31 |
32 |
33 | ## ⚙️ Tech Stack
34 |
35 | - React.js
36 | - Three.js
37 | - React Three Fiber
38 | - React Three Drei
39 | - GSAP (Greensock)
40 | - Vite
41 | - Tailwind CSS
42 |
43 | ## 🔋 Features
44 |
45 | 👉 **Beautiful Subtle Smooth Animations using GSAP**: Enhanced user experience with seamless and captivating animations powered by GSAP.
46 |
47 | 👉 **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.
48 |
49 | 👉 **Custom Video Carousel (made with GSAP)**: Engage users with a unique and interactive video carousel developed using GSAP for a personalized browsing experience.
50 |
51 | 👉 **Completely Responsive**: Consistent access and optimal viewing on any device with a fully responsive design that adapts to different screen sizes.
52 |
53 | and many more, including code architecture and reusability
54 |
55 | ## 🤸 Quick Start
56 |
57 | Follow these steps to set up the project locally on your machine.
58 |
59 | **Prerequisites**
60 |
61 | Make sure you have the following installed on your machine:
62 |
63 | - [Git](https://git-scm.com/)
64 | - [Node.js](https://nodejs.org/en)
65 | - [npm](https://www.npmjs.com/) (Node Package Manager)
66 |
67 | **Cloning the Repository**
68 |
69 | ```bash
70 | git clone https://github.com/JavaScript-Mastery-Pro/iphone-doc.git
71 | cd iphone-doc
72 | ```
73 |
74 | **Installation**
75 |
76 | Install the project dependencies using npm:
77 |
78 | ```bash
79 | npm install
80 | ```
81 |
82 | **Running the Project**
83 |
84 | ```bash
85 | npm run dev
86 | ```
87 |
88 | Open [http://localhost:5173](http://localhost:5173) in your browser to view the project.
89 |
90 | ## 🕸️ Snippets
91 |
92 |
93 | tailwind.config.js
94 |
95 | ```javascript
96 | /** @type {import('tailwindcss').Config} */
97 | export default {
98 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
99 | theme: {
100 | extend: {
101 | colors: {
102 | blue: "#2997FF",
103 | gray: {
104 | DEFAULT: "#86868b",
105 | 100: "#94928d",
106 | 200: "#afafaf",
107 | 300: "#42424570",
108 | },
109 | zinc: "#101010",
110 | },
111 | },
112 | },
113 | plugins: [],
114 | };
115 | ```
116 |
117 |
118 |
119 |
120 | constants/index.js
121 |
122 | ```javascript
123 | import {
124 | blackImg,
125 | blueImg,
126 | highlightFirstVideo,
127 | highlightFourthVideo,
128 | highlightSecondVideo,
129 | highlightThirdVideo,
130 | whiteImg,
131 | yellowImg,
132 | } from "../utils";
133 |
134 | export const navLists = ["Store", "Mac", "iPhone", "Support"];
135 |
136 | export const hightlightsSlides = [
137 | {
138 | id: 1,
139 | textLists: [
140 | "Enter A17 Pro.",
141 | "Game‑changing chip.",
142 | "Groundbreaking performance.",
143 | ],
144 | video: highlightFirstVideo,
145 | videoDuration: 4,
146 | },
147 | {
148 | id: 2,
149 | textLists: ["Titanium.", "So strong. So light. So Pro."],
150 | video: highlightSecondVideo,
151 | videoDuration: 5,
152 | },
153 | {
154 | id: 3,
155 | textLists: [
156 | "iPhone 15 Pro Max has the",
157 | "longest optical zoom in",
158 | "iPhone ever. Far out.",
159 | ],
160 | video: highlightThirdVideo,
161 | videoDuration: 2,
162 | },
163 | {
164 | id: 4,
165 | textLists: ["All-new Action button.", "What will yours do?."],
166 | video: highlightFourthVideo,
167 | videoDuration: 3.63,
168 | },
169 | ];
170 |
171 | export const models = [
172 | {
173 | id: 1,
174 | title: "iPhone 15 Pro in Natural Titanium",
175 | color: ["#8F8A81", "#ffe7b9", "#6f6c64"],
176 | img: yellowImg,
177 | },
178 | {
179 | id: 2,
180 | title: "iPhone 15 Pro in Blue Titanium",
181 | color: ["#53596E", "#6395ff", "#21242e"],
182 | img: blueImg,
183 | },
184 | {
185 | id: 3,
186 | title: "iPhone 15 Pro in White Titanium",
187 | color: ["#C9C8C2", "#ffffff", "#C9C8C2"],
188 | img: whiteImg,
189 | },
190 | {
191 | id: 4,
192 | title: "iPhone 15 Pro in Black Titanium",
193 | color: ["#454749", "#3b3b3b", "#181819"],
194 | img: blackImg,
195 | },
196 | ];
197 |
198 | export const sizes = [
199 | { label: '6.1"', value: "small" },
200 | { label: '6.7"', value: "large" },
201 | ];
202 |
203 | export const footerLinks = [
204 | "Privacy Policy",
205 | "Terms of Use",
206 | "Sales Policy",
207 | "Legal",
208 | "Site Map",
209 | ];
210 | ```
211 |
212 |
213 |
214 |
215 | Lights.jsx
216 |
217 | ```javascript
218 | import { Environment, Lightformer } from "@react-three/drei";
219 |
220 | const Lights = () => {
221 | return (
222 | // group different lights and lightformers. We can use group to organize lights, cameras, meshes, and other objects in the scene.
223 |
224 | {/**
225 | * @description Environment is used to create a background environment for the scene
226 | * https://github.com/pmndrs/drei?tab=readme-ov-file#environment
227 | */}
228 |
229 |
230 | {/**
231 | * @description Lightformer used to create custom lights with various shapes and properties in a 3D scene.
232 | * https://github.com/pmndrs/drei?tab=readme-ov-file#lightformer
233 | */}
234 |
241 |
248 |
255 |
256 |
257 |
258 | {/**
259 | * @description spotLight is used to create a light source positioned at a specific point
260 | * in the scene that emits light in a specific direction.
261 | * https://threejs.org/docs/#api/en/lights/SpotLight
262 | */}
263 |
271 |
279 |
286 |
287 | );
288 | };
289 |
290 | export default Lights;
291 | ```
292 |
293 |
294 |
295 | materials
296 |
297 | ```javascript
298 | useEffect(() => {
299 | Object.entries(materials).map((material) => {
300 | // these are the material names that can't be changed color
301 | if (
302 | material[0] !== "zFdeDaGNRwzccye" &&
303 | material[0] !== "ujsvqBWRMnqdwPx" &&
304 | material[0] !== "hUlRcbieVuIiOXG" &&
305 | material[0] !== "jlzuBkUzuJqgiAK" &&
306 | material[0] !== "xNrofRCqOXXHVZt"
307 | ) {
308 | material[1].color = new THREE.Color(props.item.color[0]);
309 | }
310 | material[1].needsUpdate = true;
311 | });
312 | }, [materials, props.item]);
313 | ```
314 |
315 |
316 |
317 |
318 | VideoCarousel.jsx
319 |
320 | ```javascript
321 | import gsap from "gsap";
322 | import { useGSAP } from "@gsap/react";
323 | import { ScrollTrigger } from "gsap/all";
324 | gsap.registerPlugin(ScrollTrigger);
325 | import { useEffect, useRef, useState } from "react";
326 |
327 | import { hightlightsSlides } from "../constants";
328 | import { pauseImg, playImg, replayImg } from "../utils";
329 |
330 | const VideoCarousel = () => {
331 | const videoRef = useRef([]);
332 | const videoSpanRef = useRef([]);
333 | const videoDivRef = useRef([]);
334 |
335 | // video and indicator
336 | const [video, setVideo] = useState({
337 | isEnd: false,
338 | startPlay: false,
339 | videoId: 0,
340 | isLastVideo: false,
341 | isPlaying: false,
342 | });
343 |
344 | const [loadedData, setLoadedData] = useState([]);
345 | const { isEnd, isLastVideo, startPlay, videoId, isPlaying } = video;
346 |
347 | useGSAP(() => {
348 | // slider animation to move the video out of the screen and bring the next video in
349 | gsap.to("#slider", {
350 | transform: `translateX(${-100 * videoId}%)`,
351 | duration: 2,
352 | ease: "power2.inOut", // show visualizer https://gsap.com/docs/v3/Eases
353 | });
354 |
355 | // video animation to play the video when it is in the view
356 | gsap.to("#video", {
357 | scrollTrigger: {
358 | trigger: "#video",
359 | toggleActions: "restart none none none",
360 | },
361 | onComplete: () => {
362 | setVideo((pre) => ({
363 | ...pre,
364 | startPlay: true,
365 | isPlaying: true,
366 | }));
367 | },
368 | });
369 | }, [isEnd, videoId]);
370 |
371 | useEffect(() => {
372 | let currentProgress = 0;
373 | let span = videoSpanRef.current;
374 |
375 | if (span[videoId]) {
376 | // animation to move the indicator
377 | let anim = gsap.to(span[videoId], {
378 | onUpdate: () => {
379 | // get the progress of the video
380 | const progress = Math.ceil(anim.progress() * 100);
381 |
382 | if (progress != currentProgress) {
383 | currentProgress = progress;
384 |
385 | // set the width of the progress bar
386 | gsap.to(videoDivRef.current[videoId], {
387 | width:
388 | window.innerWidth < 760
389 | ? "10vw" // mobile
390 | : window.innerWidth < 1200
391 | ? "10vw" // tablet
392 | : "4vw", // laptop
393 | });
394 |
395 | // set the background color of the progress bar
396 | gsap.to(span[videoId], {
397 | width: `${currentProgress}%`,
398 | backgroundColor: "white",
399 | });
400 | }
401 | },
402 |
403 | // when the video is ended, replace the progress bar with the indicator and change the background color
404 | onComplete: () => {
405 | if (isPlaying) {
406 | gsap.to(videoDivRef.current[videoId], {
407 | width: "12px",
408 | });
409 | gsap.to(span[videoId], {
410 | backgroundColor: "#afafaf",
411 | });
412 | }
413 | },
414 | });
415 |
416 | if (videoId == 0) {
417 | anim.restart();
418 | }
419 |
420 | // update the progress bar
421 | const animUpdate = () => {
422 | anim.progress(
423 | videoRef.current[videoId].currentTime /
424 | hightlightsSlides[videoId].videoDuration
425 | );
426 | };
427 |
428 | if (isPlaying) {
429 | // ticker to update the progress bar
430 | gsap.ticker.add(animUpdate);
431 | } else {
432 | // remove the ticker when the video is paused (progress bar is stopped)
433 | gsap.ticker.remove(animUpdate);
434 | }
435 | }
436 | }, [videoId, startPlay]);
437 |
438 | useEffect(() => {
439 | if (loadedData.length > 3) {
440 | if (!isPlaying) {
441 | videoRef.current[videoId].pause();
442 | } else {
443 | startPlay && videoRef.current[videoId].play();
444 | }
445 | }
446 | }, [startPlay, videoId, isPlaying, loadedData]);
447 |
448 | // vd id is the id for every video until id becomes number 3
449 | const handleProcess = (type, i) => {
450 | switch (type) {
451 | case "video-end":
452 | setVideo((pre) => ({ ...pre, isEnd: true, videoId: i + 1 }));
453 | break;
454 |
455 | case "video-last":
456 | setVideo((pre) => ({ ...pre, isLastVideo: true }));
457 | break;
458 |
459 | case "video-reset":
460 | setVideo((pre) => ({ ...pre, videoId: 0, isLastVideo: false }));
461 | break;
462 |
463 | case "pause":
464 | setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying }));
465 | break;
466 |
467 | case "play":
468 | setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying }));
469 | break;
470 |
471 | default:
472 | return video;
473 | }
474 | };
475 |
476 | const handleLoadedMetaData = (i, e) => setLoadedData((pre) => [...pre, e]);
477 |
478 | return (
479 | <>
480 |
481 | {hightlightsSlides.map((list, i) => (
482 |
483 |
484 |
485 | (videoRef.current[i] = el)}
494 | onEnded={() =>
495 | i !== 3
496 | ? handleProcess("video-end", i)
497 | : handleProcess("video-last")
498 | }
499 | onPlay={() =>
500 | setVideo((pre) => ({ ...pre, isPlaying: true }))
501 | }
502 | onLoadedMetadata={(e) => handleLoadedMetaData(i, e)}
503 | >
504 |
505 |
506 |
507 |
508 |
509 | {list.textLists.map((text, i) => (
510 |
511 | {text}
512 |
513 | ))}
514 |
515 |
516 |
517 | ))}
518 |
519 |
520 |
521 |
522 | {videoRef.current.map((_, i) => (
523 | (videoDivRef.current[i] = el)}
527 | >
528 | (videoSpanRef.current[i] = el)}
531 | />
532 |
533 | ))}
534 |
535 |
536 |
537 | handleProcess("video-reset")
543 | : !isPlaying
544 | ? () => handleProcess("play")
545 | : () => handleProcess("pause")
546 | }
547 | />
548 |
549 |
550 | >
551 | );
552 | };
553 |
554 | export default VideoCarousel;
555 |
556 | ```
557 |
558 |
559 |
560 |
561 | utils/index.js
562 |
563 | ```javascript
564 | import hero from "/assets/images/hero.jpeg";
565 |
566 | export const heroImg = hero;
567 |
568 | import hmv from "/assets/videos/hero.mp4";
569 | import smallmv from "/assets/videos/smallHero.mp4";
570 | import highlightFirstmv from "/assets/videos/highlight-first.mp4";
571 | import highlightSectmv from "/assets/videos/hightlight-third.mp4";
572 | import highlightThirdmv from "/assets/videos/hightlight-sec.mp4";
573 | import highlightFourthmv from "/assets/videos/hightlight-fourth.mp4";
574 | import exploremv from "/assets/videos/explore.mp4";
575 | import framemv from "/assets/videos/frame.mp4";
576 |
577 | import apple from "/assets/images/apple.svg";
578 | import search from "/assets/images/search.svg";
579 | import bag from "/assets/images/bag.svg";
580 | import watch from "/assets/images/watch.svg";
581 | import right from "/assets/images/right.svg";
582 | import replay from "/assets/images/replay.svg";
583 | import play from "/assets/images/play.svg";
584 | import pause from "/assets/images/pause.svg";
585 |
586 | import yellow from "/assets/images/yellow.jpg";
587 | import blue from "/assets/images/blue.jpg";
588 | import white from "/assets/images/white.jpg";
589 | import black from "/assets/images/black.jpg";
590 | import explore1 from "/assets/images/explore1.jpg";
591 | import explore2 from "/assets/images/explore2.jpg";
592 | import chip from "/assets/images/chip.jpeg";
593 | import frame from "/assets/images/frame.png";
594 |
595 | export const heroVideo = hmv;
596 | export const smallHeroVideo = smallmv;
597 | export const highlightFirstVideo = highlightFirstmv;
598 | export const highlightSecondVideo = highlightSectmv;
599 | export const highlightThirdVideo = highlightThirdmv;
600 | export const highlightFourthVideo = highlightFourthmv;
601 | export const exploreVideo = exploremv;
602 | export const frameVideo = framemv;
603 |
604 | export const appleImg = apple;
605 | export const searchImg = search;
606 | export const bagImg = bag;
607 | export const watchImg = watch;
608 | export const rightImg = right;
609 | export const replayImg = replay;
610 | export const playImg = play;
611 | export const pauseImg = pause;
612 |
613 | export const yellowImg = yellow;
614 | export const blueImg = blue;
615 | export const whiteImg = white;
616 | export const blackImg = black;
617 | export const explore1Img = explore1;
618 | export const explore2Img = explore2;
619 | export const chipImg = chip;
620 | export const frameImg = frame;
621 | ```
622 |
623 |
624 |
625 | index.css
626 |
627 | ```css
628 | @tailwind base;
629 | @tailwind components;
630 | @tailwind utilities;
631 |
632 | * {
633 | margin: 0;
634 | padding: 0;
635 | box-sizing: border-box;
636 | }
637 |
638 | body {
639 | color: white;
640 | width: 100dvw;
641 | overflow-x: hidden;
642 | height: 100%;
643 | background: #000;
644 | border-color: #3b3b3b;
645 | user-select: none;
646 | }
647 |
648 | canvas {
649 | touch-action: none;
650 | }
651 |
652 | .scrim-max-width {
653 | margin-inline-start: auto;
654 | margin-inline-end: auto;
655 | position: relative;
656 | max-width: 1120px;
657 | }
658 |
659 | @layer utilities {
660 | .flex-center {
661 | @apply flex items-center justify-center
662 | }
663 |
664 | .nav-height {
665 | @apply h-[calc(100vh-60px)]
666 | }
667 |
668 | .btn {
669 | @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
670 | }
671 |
672 | .color-container {
673 | @apply flex items-center justify-center px-4 py-4 rounded-full bg-gray-300 backdrop-blur
674 | }
675 |
676 | .size-btn-container {
677 | @apply flex items-center justify-center p-1 rounded-full bg-gray-300 backdrop-blur ml-3 gap-1
678 | }
679 |
680 | .size-btn {
681 | @apply w-10 h-10 text-sm flex justify-center items-center bg-white text-black rounded-full transition-all
682 | }
683 |
684 | .common-padding {
685 | @apply sm:py-32 py-20 sm:px-10 px-5
686 | }
687 |
688 | .section-heading {
689 | @apply text-gray lg:text-6xl md:text-5xl text-3xl lg:mb-0 mb-5 font-medium opacity-0 translate-y-20
690 | }
691 |
692 | .feature-text {
693 | @apply text-gray max-w-md text-lg md:text-xl font-semibold opacity-0 translate-y-[100px]
694 | }
695 |
696 | .feature-text-container {
697 | @apply w-full flex-center flex-col md:flex-row mt-10 md:mt-16 gap-5
698 | }
699 |
700 | .feature-video {
701 | @apply w-full h-full object-cover object-center scale-150 opacity-0
702 | }
703 |
704 | .feature-video-container {
705 | @apply w-full flex flex-col md:flex-row gap-5 items-center
706 | }
707 |
708 | .link {
709 | @apply text-blue hover:underline cursor-pointer flex items-center text-xl opacity-0 translate-y-20
710 | }
711 |
712 | .control-btn {
713 | @apply ml-4 p-4 rounded-full bg-gray-300 backdrop-blur flex-center
714 | }
715 |
716 | .hero-title {
717 | @apply text-center font-semibold text-3xl text-gray-100 opacity-0 max-md:mb-10
718 | }
719 |
720 | .hiw-title {
721 | @apply text-4xl md:text-7xl font-semibold text-center
722 | }
723 |
724 | .hiw-subtitle {
725 | @apply text-gray font-semibold text-xl md:text-2xl py-10 text-center
726 | }
727 |
728 | .hiw-video {
729 | @apply absolute w-[95%] h-[90%] rounded-[56px] overflow-hidden
730 | }
731 |
732 | .hiw-text-container {
733 | @apply flex md:flex-row flex-col justify-between items-start gap-24
734 | }
735 |
736 | .hiw-text {
737 | @apply text-gray text-xl font-normal md:font-semibold
738 | }
739 |
740 | .hiw-bigtext {
741 | @apply text-white text-3xl md:text-5xl font-normal md:font-semibold my-2
742 | }
743 |
744 | .video-carousel_container {
745 | @apply relative sm:w-[70vw] w-[88vw] md:h-[70vh] sm:h-[50vh] h-[35vh]
746 | }
747 |
748 | .g_fadeIn {
749 | @apply opacity-0 translate-y-[100px]
750 | }
751 | }
752 | ```
753 |
754 |
755 |
756 | ## 🔗 Links
757 |
758 | Public Assets used in the project can be found [here](https://drive.google.com/file/d/1syHiNxSIGXVApaIozdrLXM2x5dPhvaJL/view?usp=sharing)
759 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Apple iPhone
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apple-website",
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.0",
14 | "@react-three/drei": "^9.101.0",
15 | "@react-three/fiber": "^8.15.19",
16 | "@sentry/react": "^7.106.0",
17 | "@sentry/vite-plugin": "^2.14.3",
18 | "gsap": "^3.12.5",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "three": "^0.162.0"
22 | },
23 | "devDependencies": {
24 | "@types/react": "^18.2.56",
25 | "@types/react-dom": "^18.2.19",
26 | "@vitejs/plugin-react": "^4.2.1",
27 | "autoprefixer": "^10.4.18",
28 | "eslint": "^8.56.0",
29 | "eslint-plugin-react": "^7.33.2",
30 | "eslint-plugin-react-hooks": "^4.6.0",
31 | "eslint-plugin-react-refresh": "^0.4.5",
32 | "postcss": "^8.4.35",
33 | "tailwindcss": "^3.4.1",
34 | "vite": "^5.1.4"
35 | }
36 | }
--------------------------------------------------------------------------------
/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/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/black.jpg
--------------------------------------------------------------------------------
/public/assets/images/blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/blue.jpg
--------------------------------------------------------------------------------
/public/assets/images/chip.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/chip.jpeg
--------------------------------------------------------------------------------
/public/assets/images/explore1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/explore1.jpg
--------------------------------------------------------------------------------
/public/assets/images/explore2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/explore2.jpg
--------------------------------------------------------------------------------
/public/assets/images/frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/frame.png
--------------------------------------------------------------------------------
/public/assets/images/hero.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/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/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/white.jpg
--------------------------------------------------------------------------------
/public/assets/images/yellow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/images/yellow.jpg
--------------------------------------------------------------------------------
/public/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/videos/explore.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/explore.mp4
--------------------------------------------------------------------------------
/public/assets/videos/frame.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/frame.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hero.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/hero.mp4
--------------------------------------------------------------------------------
/public/assets/videos/highlight-first.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/highlight-first.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hightlight-fourth.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/hightlight-fourth.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hightlight-sec.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/hightlight-sec.mp4
--------------------------------------------------------------------------------
/public/assets/videos/hightlight-third.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/hightlight-third.mp4
--------------------------------------------------------------------------------
/public/assets/videos/smallHero.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/assets/videos/smallHero.mp4
--------------------------------------------------------------------------------
/public/models/scene.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/public/models/scene.glb
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/syedahmedullah14/Apple-website/086af093eda3366db87aa7280e0296ec4ffd4791/src/App.css
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import './App.css'
2 | import Navbar from './components/Navbar'
3 | import Hero from './components/Hero'
4 | import Highlights from './components/Highlights'
5 | import Model from './components/Model'
6 | import Features from './components/Features'
7 | import HowItWorks from './components/HowItWorks'
8 | import Footer from './components/Footer'
9 |
10 | const App = () => {
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default App
26 |
--------------------------------------------------------------------------------
/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 |
7 | const Features = () => {
8 | const videoRef = useRef();
9 |
10 | useGSAP(() => {
11 | gsap.to('#exploreVideo', {
12 | scrollTrigger: {
13 | trigger: '#exploreVideo',
14 | toggleActions: 'play pause reverse restart',
15 | start: '-10% bottom',
16 | },
17 | onComplete: () => {
18 | videoRef.current.play();
19 | }
20 | })
21 |
22 | animateWithGsap('#features_title', { y:0, opacity:1})
23 | animateWithGsap(
24 | '.g_grow',
25 | { scale: 1, opacity: 1, ease: 'power1' },
26 | { scrub: 5.5 }
27 | );
28 | animateWithGsap(
29 | '.g_text',
30 | {y:0, opacity: 1,ease: 'power2.inOut',duration: 1}
31 | )
32 | }, []);
33 |
34 | return (
35 |
36 |
37 |
38 |
Explore the full story.
39 |
40 |
41 |
42 |
43 |
iPhone.
44 | Forged in titanium.
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | iPhone 15 Pro is {' '}
68 |
69 | the first iPhone to feature an aerospace-grade titanium design
70 | ,
71 | using the same alloy that spacecrafts use for missions to Mars.
72 |
73 |
74 |
75 |
76 |
77 | Titanium has one of the best strength-to-weight ratios of any metal, making these our {' '}
78 |
79 | lightest Pro models ever.
80 |
81 | You'll notice the difference the moment you pick one up.
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | )
93 | }
94 |
95 | export default Features
--------------------------------------------------------------------------------
/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 | More ways to shop: {' '}
11 |
12 | Find an Apple Store {' '}
13 |
14 | or {' '}
15 |
16 | other retailer
17 | {' '}
18 | near you.
19 |
20 |
21 | Or call 000800-040-1966
22 |
23 |
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 | export default Footer
--------------------------------------------------------------------------------
/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 |
6 | const Hero = () => {
7 | const [videoSrc, setVideoSrc] = useState(window.innerWidth < 760 ? smallHeroVideo : heroVideo)
8 |
9 | const handleVideoSrcSet = () => {
10 | if(window.innerWidth < 760) {
11 | setVideoSrc(smallHeroVideo)
12 | } else {
13 | setVideoSrc(heroVideo)
14 | }
15 | }
16 |
17 | useEffect(() => {
18 | window.addEventListener('resize', handleVideoSrcSet);
19 |
20 | return () => {
21 | window.removeEventListener('reisze', handleVideoSrcSet)
22 | }
23 | }, [])
24 |
25 | useGSAP(() => {
26 | gsap.to('#hero', { opacity: 1, delay: 2 })
27 | gsap.to('#cta', { opacity: 1, y: -50, delay: 2 })
28 | }, [])
29 |
30 | return (
31 |
32 |
33 |
iPhone 15 Pro
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
45 |
Buy
46 |
From $199/month or $999
47 |
48 |
49 | )
50 | }
51 |
52 | export default Hero
--------------------------------------------------------------------------------
/src/components/Highlights.jsx:
--------------------------------------------------------------------------------
1 | import { useGSAP } from "@gsap/react"
2 | import gsap from "gsap"
3 | import { rightImg, watchImg } from "../utils"
4 |
5 | import VideoCarousel from './VideoCarousel';
6 |
7 | const Highlights = () => {
8 | useGSAP(() => {
9 | gsap.to('#title', { opacity: 1, y: 0 })
10 | gsap.to('.link', { opacity: 1, y: 0, duration: 1, stagger: 0.25 })
11 | }, [])
12 |
13 | return (
14 |
15 |
16 |
17 |
Get the highlights.
18 |
19 |
20 |
21 | Watch the film
22 |
23 |
24 |
25 | Watch the event
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default Highlights
--------------------------------------------------------------------------------
/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 | const videoRef = useRef();
9 |
10 | useGSAP(() => {
11 | gsap.from('#chip', {
12 | scrollTrigger: {
13 | trigger: '#chip',
14 | start: '20% bottom'
15 | },
16 | opacity: 0,
17 | scale: 2,
18 | duration: 2,
19 | ease: 'power2.inOut'
20 | })
21 |
22 | animateWithGsap('.g_fadeIn', {
23 | opacity: 1,
24 | y: 0,
25 | duration: 1,
26 | ease: 'power2.inOut'
27 | })
28 | }, []);
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | A17 Pro chip.
40 | A monster win for gaming.
41 |
42 |
43 |
44 | It's here. The biggest redesign in the history of Apple GPUs.
45 |
46 |
47 |
48 |
49 |
50 |
51 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
Honkai: Star Rail
64 |
65 |
66 |
67 |
68 |
69 | A17 Pro is an entirely new class of iPhone chip that delivers our {' '}
70 |
71 | best graphic performance by far
72 | .
73 |
74 |
75 |
76 | Mobile {' '}
77 |
78 | games will look and feel so immersive
79 | ,
80 | with incredibly detailed environments and characters.
81 |
82 |
83 |
84 |
85 |
86 |
New
87 |
Pro-class GPU
88 |
with 6 cores
89 |
90 |
91 |
92 |
93 | )
94 | }
95 |
96 | export default HowItWorks
--------------------------------------------------------------------------------
/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 { appleImg, bagImg, searchImg } from '../utils';
2 | import { navLists } from '../constants';
3 |
4 | const Navbar = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | {navLists.map((nav) => (
12 |
13 | {nav}
14 |
15 | ))}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default Navbar
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {
6 | colors: {
7 | blue: "#2997FF",
8 | gray: {
9 | DEFAULT: "#86868b",
10 | 100: "#94928d",
11 | 200: "#afafaf",
12 | 300: "#42424570",
13 | },
14 | zinc: "#101010",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------