Bhabananda Das commited on
Commit
bd903ab
·
1 Parent(s): 3efcb14
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # production
12
+ /build
13
+
14
+ # misc
15
+ .env
16
+ .DS_Store
17
+ .env.local
18
+ .env.development.local
19
+ .env.test.local
20
+ .env.production.local
21
+
22
+ npm-debug.log*
23
+ yarn-debug.log*
24
+ yarn-error.log*
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+ RUN apt-get update -y && apt-get upgrade -y \
3
+ && apt-get clean \
4
+ && rm -rf /var/lib/apt/lists/*
5
+
6
+ COPY . /wd
7
+
8
+ RUN chmod 777 /wd
9
+ WORKDIR /wd
10
+
11
+ RUN npm install
12
+
13
+ CMD ["npm", "start"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Shivraj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tubeone",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@chakra-ui/react": "^2.8.0",
7
+ "@emotion/react": "^11.11.1",
8
+ "@emotion/styled": "^11.11.0",
9
+ "@testing-library/jest-dom": "^5.17.0",
10
+ "@testing-library/react": "^13.4.0",
11
+ "@testing-library/user-event": "^13.5.0",
12
+ "axios": "^1.7.2",
13
+ "date-fns": "^2.30.0",
14
+ "framer-motion": "^10.18.0",
15
+ "lodash": "^4.17.21",
16
+ "numeral": "^2.0.6",
17
+ "react": "^18.2.0",
18
+ "react-dom": "^18.2.0",
19
+ "react-ga4": "^2.1.0",
20
+ "react-icons": "^4.12.0",
21
+ "react-infinite-scroll-component": "^6.1.0",
22
+ "react-player": "^2.16.0",
23
+ "react-router-dom": "^6.25.1",
24
+ "react-scripts": "5.0.1",
25
+ "react-youtube": "^10.1.0",
26
+ "web-vitals": "^2.1.4"
27
+ },
28
+ "scripts": {
29
+ "start": "PORT=7860 react-scripts start",
30
+ "build": "react-scripts build",
31
+ "test": "react-scripts test",
32
+ "eject": "react-scripts eject",
33
+ "lint": "eslint \"src/**/*.{js,jsx}\"",
34
+ "lint:fix": "eslint \"src/**/*.{js,jsx}\" --fix"
35
+ },
36
+ "eslintConfig": {
37
+ "extends": [
38
+ "react-app",
39
+ "react-app/jest"
40
+ ]
41
+ },
42
+ "browserslist": {
43
+ "production": [
44
+ ">0.2%",
45
+ "not dead",
46
+ "not op_mini all"
47
+ ],
48
+ "development": [
49
+ "last 1 chrome version",
50
+ "last 1 firefox version",
51
+ "last 1 safari version"
52
+ ]
53
+ },
54
+ "devDependencies": {
55
+ "eslint": "^8.46.0",
56
+ "eslint-plugin-import": "^2.28.0",
57
+ "eslint-plugin-jsx-a11y": "^6.7.1",
58
+ "eslint-plugin-react": "^7.33.1"
59
+ }
60
+ }
public/favicon.ico ADDED
public/index.html ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
+ <meta name="theme-color" content="#000000" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+
9
+ <meta name="description" content="React Youtube clone using YouTube api" />
10
+ <meta
11
+ name="keywords"
12
+ content="YouTube Clone, YouTube Clone React, YouTube Clone Reactjs, YouTube Clone using YouTube Api, YouTube Clone using Google Api, YouTube Clone using Rapid Api, YouTube Clone using YouTube v3 Api, Online Videos, YouTube Alternative, All Coutries Trending Videos"
13
+ />
14
+ <meta name="author" content="Shivraj Gurjar" />
15
+
16
+ <!-- Open Graph Meta Tags (for social media sharing) -->
17
+ <meta property="og:title" content="YouTube Clone" />
18
+ <meta
19
+ property="og:description"
20
+ content="React Youtube clone using YouTube api"
21
+ />
22
+ <meta property="og:image" content="path/to/your-thumbnail-image.jpg" />
23
+ <meta
24
+ property="og:url"
25
+ content="https://youtube-clone-shivraj.vercel.app"
26
+ />
27
+
28
+ <!-- Twitter Card Meta Tags -->
29
+ <meta name="twitter:card" content="summary_large_image" />
30
+ <meta name="twitter:title" content="YouTube Clone" />
31
+ <meta
32
+ name="twitter:description"
33
+ content="React Youtube clone using YouTube api"
34
+ />
35
+ <meta name="twitter:image" content="path/to/your-thumbnail-image.jpg" />
36
+
37
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
38
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
39
+
40
+ <title>YouTube Clone</title>
41
+ </head>
42
+ <body>
43
+ <noscript>You need to enable JavaScript to run this app.</noscript>
44
+ <div id="root"></div>
45
+ </body>
46
+ </html>
public/logo256.png ADDED
public/logo32.png ADDED
public/logo512.png ADDED
public/logo72.png ADDED
public/manifest.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "Youtube Clone",
3
+ "name": "Youtube clone by Shiv",
4
+ "description":"This is React YouTube Clone Where you can stream trending videos of different countries with infinite scrolling, you can search videos and you can choose any section in to sidebar which you want",
5
+ "icons": [
6
+ {
7
+ "src": "favicon.ico",
8
+ "sizes": "512x512",
9
+ "type": "image/x-icon"
10
+ },
11
+ {
12
+ "src": "logo32.png",
13
+ "type": "image/png",
14
+ "sizes": "32x32"
15
+ },
16
+ {
17
+ "src": "logo72.png",
18
+ "type": "image/png",
19
+ "sizes": "72x72"
20
+ },
21
+ {
22
+ "src": "logo256.png",
23
+ "type": "image/png",
24
+ "sizes": "256x256"
25
+ },
26
+ {
27
+ "src": "logo512.png",
28
+ "type": "image/png",
29
+ "sizes": "512x512"
30
+ }
31
+ ],
32
+ "background_color": "#0f0f0f",
33
+ "theme_color": "black",
34
+ "display": "standalone",
35
+ "scope": "/",
36
+ "start_url": "/",
37
+ "categories": ["Entertainment", "Video"],
38
+ "prefer_related_applications": false,
39
+ "dir": "ltr",
40
+ "lang": "en-US"
41
+
42
+ }
public/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
src/App.css ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ div {
2
+ /* color: #0000006d; */
3
+ }
4
+
5
+ /* Customize the scrollbar track */
6
+ ::-webkit-scrollbar {
7
+ width: 8px; /* Width of the scrollbar */
8
+ }
9
+
10
+ /* Customize the scrollbar thumb */
11
+ ::-webkit-scrollbar-thumb {
12
+ background: #959595; /* Color of the scrollbar thumb */
13
+ border-radius: 5px; /* Rounded corners for the thumb */
14
+ margin: 2px 4px;
15
+ }
16
+
17
+ /* Change the color when hovering over the scrollbar thumb */
18
+
19
+ ::-webkit-scrollbar-thumb:hover {
20
+ background: #717171;
21
+ }
22
+
23
+ ::-webkit-scrollbar-track {
24
+ background: #0f0f0f; /* Change this color to the desired color */
25
+ }
26
+
27
+ .youtube-video {
28
+ width: 860px;
29
+ height: 485px;
30
+ }
31
+
32
+ /* Hide scrollbar on mobile screens */
33
+ @media (max-width: 767px) {
34
+ .youtube-video {
35
+ width: 100%;
36
+ height: 300px;
37
+ }
38
+ .hidden-scrollbar {
39
+ overflow-x: auto;
40
+ /* hide scrollbar for IE, Edge and Firefox */
41
+ -ms-overflow-style: none;
42
+ scrollbar-width: none;
43
+ }
44
+
45
+ /* hide scrollbar for chrome, safari and opera */
46
+ .hidden-scrollbar::-webkit-scrollbar {
47
+ display: none;
48
+ }
49
+
50
+ ::-webkit-scrollbar {
51
+ width: 0px; /* Width of the scrollbar */
52
+ }
53
+ }
src/App.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Fragment } from "react";
2
+ import { BrowserRouter, Route, Routes } from "react-router-dom";
3
+
4
+ import Video from "./pages/video/Video";
5
+ import Home from "./pages/home/Home";
6
+ import "./App.css";
7
+ import MobileSearch from "./components/layout/MobileSearch";
8
+
9
+ function App() {
10
+
11
+ return (
12
+ <Fragment>
13
+ <BrowserRouter>
14
+ <Routes>
15
+ <Route path='/' element={<Home />} />
16
+ <Route path='/video/:videoId/:channelId' element={<Video />} />
17
+ <Route path='/search/mobile' element={<MobileSearch />} />
18
+ </Routes>
19
+ </BrowserRouter>
20
+ </Fragment>
21
+ );
22
+ }
23
+
24
+ export default App;
src/components/cards/HomeVideoCard.jsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Avatar, Box, Flex, Image, Text } from "@chakra-ui/react";
3
+ import { NavLink } from "react-router-dom";
4
+
5
+ const HomeVideoCard = ({
6
+ videoId,
7
+ thumbnail,
8
+ title,
9
+ postTime,
10
+ views,
11
+ duration,
12
+ avatar,
13
+ channelName,
14
+ channelId,
15
+ }) => (
16
+ <>
17
+ <NavLink to={`/video/${videoId}/${channelId}`}>
18
+ <Box>
19
+ <Box
20
+ borderRadius={{ base: "0px", sm: "0px", md: "8px" }}
21
+ position={"relative"}
22
+ backgroundRepeat={"no-repeat"}
23
+ height={{ base: "210px", sm: "270px", md: "200px" }}
24
+ backgroundImage={thumbnail}
25
+ backgroundSize={"cover"}
26
+ backgroundColor="#2e2c2c"
27
+ backgroundPosition={"center"}
28
+ >
29
+ <Text
30
+ position={"absolute"}
31
+ right={3}
32
+ bottom={3}
33
+ color={"white"}
34
+ background={"black"}
35
+ fontSize={"xs"}
36
+ padding={"3px 8px"}
37
+ borderRadius={"5px"}
38
+ >
39
+ {duration || "0:06:23"}
40
+ </Text>
41
+ </Box>
42
+
43
+ <Flex padding={{ base: "10px", sm: "10px", md: "10px 0 0 0" }} gap={3}>
44
+ {avatar ? (
45
+ <Image
46
+ height={"35px"}
47
+ borderRadius={"100%"}
48
+ src={avatar}
49
+ alt="channel"
50
+ />
51
+ ) : (
52
+ <Avatar
53
+ size={"sm"}
54
+ name={channelName}
55
+ src="https://bit.ly/tioluwani-kolawole"
56
+ />
57
+ )}
58
+
59
+ <Box color={"white"}>
60
+ <Text fontWeight={"semibold"}>{title}</Text>
61
+ <Box
62
+ marginTop={"5px"}
63
+ gap={3}
64
+ display={{ base: "flex", sm: "flex", md: "block" }}
65
+ >
66
+ <Text color={"#b7b5b5"} fontSize={"xs"}>
67
+ {channelName}
68
+ </Text>
69
+ <Flex margintop="5px" gap={2}>
70
+ <Text color={"#b7b5b5"} fontSize={"xs"}>
71
+ {views ? `${views} views` : ""}
72
+ </Text>
73
+ <Text color={"#b7b5b5"} fontSize={"xs"}>
74
+ {postTime || ""}
75
+ </Text>
76
+ </Flex>
77
+ </Box>
78
+ </Box>
79
+ </Flex>
80
+ </Box>
81
+ </NavLink>
82
+ </>
83
+ );
84
+
85
+ export default HomeVideoCard;
src/components/cards/RelatedVideoCard.jsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Box, Flex, Image, Text } from "@chakra-ui/react";
2
+ import React from "react";
3
+ import { NavLink } from "react-router-dom";
4
+
5
+ const RelatedVideoCard = ({
6
+ videoId,
7
+ title,
8
+ thumbnail,
9
+ duration,
10
+ views,
11
+ postTime,
12
+ channelName,
13
+ channelId,
14
+ }) => (
15
+ <>
16
+ <NavLink to={`/video/${videoId}/${channelId}?category="Trending`}>
17
+ <Flex alignItems={"center"} width={"100%"} gap={5}>
18
+ <Box
19
+ backgroundColor="#2e2c2c"
20
+ borderRadius={"8px"}
21
+ position={"relative"}
22
+ overflow={"hidden"}
23
+ width={"140px"}
24
+ height={"70px"}
25
+ >
26
+ <Image
27
+ src={thumbnail}
28
+ alt="thumbnail"
29
+ objectFit={"cover"}
30
+ width={"100%"}
31
+ />
32
+ <Text
33
+ right={3}
34
+ bottom={3}
35
+ color={"white"}
36
+ background={"black"}
37
+ fontSize={"xs"}
38
+ padding={"3px 8px"}
39
+ borderRadius={"5px"}
40
+ position={"absolute"}
41
+ >
42
+ {duration || "0:00"}
43
+ </Text>
44
+ </Box>
45
+
46
+ <Box width="70%">
47
+ <Text fontSize={"16px"} color={"white"}>
48
+ {title}
49
+ </Text>
50
+ <Text color={"#b7b5b5"} fontSize={"xs"}>
51
+ {views || ""} {postTime || "1 day ago"}
52
+ </Text>
53
+
54
+ <Flex gap={3} alignItems={"center"}>
55
+ <Text color={"#b7b5b5"} fontSize={"sm"}>
56
+ {channelName}
57
+ </Text>
58
+ </Flex>
59
+ </Box>
60
+ </Flex>
61
+ </NavLink>
62
+ </>
63
+ );
64
+
65
+ export default RelatedVideoCard;
src/components/cards/SearchVideoCard.jsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Avatar, Box, Flex, Image, Text } from "@chakra-ui/react";
2
+ import React from "react";
3
+ import { NavLink } from "react-router-dom";
4
+
5
+ const SearchVideoCard = ({
6
+ videoId,
7
+ title,
8
+ thumbnail,
9
+ duration,
10
+ views,
11
+ postTime,
12
+ avatar,
13
+ channelName,
14
+ desc,
15
+ channelId,
16
+ }) => (
17
+ <>
18
+ <NavLink to={`/video/${videoId}/${channelId}?category=${"Trending"}`}>
19
+ <Flex width={"100%"} gap={5}>
20
+ <Box
21
+ backgroundColor="#2e2c2c"
22
+ borderRadius={"8px"}
23
+ position={"relative"}
24
+ overflow={"hidden"}
25
+ width={"30%"}
26
+ height={"170px"}
27
+ >
28
+ <Image
29
+ src={thumbnail}
30
+ width="100%"
31
+ alt="thumbnail"
32
+ objectFit={"cover"}
33
+ height={"100%"}
34
+ />
35
+ <Text
36
+ right={3}
37
+ bottom={3}
38
+ color={"white"}
39
+ background={"black"}
40
+ fontSize={"xs"}
41
+ padding={"3px 8px"}
42
+ borderRadius={"5px"}
43
+ position={"absolute"}
44
+ >
45
+ {duration || "0:00"}
46
+ </Text>
47
+ </Box>
48
+
49
+ <Box width="70%">
50
+ <Text fontSize={"22px"} color={"white"}>
51
+ {title}
52
+ </Text>
53
+
54
+ <Flex margintop="5px" gap={2}>
55
+ <Text color={"#b7b5b5"} fontSize={"sm"}>
56
+ {views || ""}{" "}
57
+ </Text>
58
+ <Text color={"#b7b5b5"} fontSize={"sm"}>
59
+ |
60
+ </Text>
61
+ <Text color={"#b7b5b5"} fontSize={"sm"}>
62
+ {postTime || "1 day ago"}
63
+ </Text>
64
+ </Flex>
65
+
66
+ <Flex marginTop={"10px"} gap={3} alignItems={"center"}>
67
+ {avatar ? (
68
+ <Image
69
+ height={"35px"}
70
+ borderRadius={"100%"}
71
+ src={avatar}
72
+ alt="channel"
73
+ />
74
+ ) : (
75
+ <Avatar
76
+ size={"sm"}
77
+ name={channelName}
78
+ src="https://bit.ly/tioluwani-kolawole"
79
+ />
80
+ )}
81
+ <Text color={"#b7b5b5"} fontSize={"sm"}>
82
+ {channelName}
83
+ </Text>
84
+ </Flex>
85
+
86
+ <Text>{desc || ""}</Text>
87
+ </Box>
88
+ </Flex>
89
+ </NavLink>
90
+ </>
91
+ );
92
+
93
+ export default SearchVideoCard;
src/components/constants/Constants.jsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { AiTwotoneFire } from "react-icons/ai";
3
+ import {
4
+ MdSportsBaseball,
5
+ MdSportsEsports,
6
+ MdOutlineHealthAndSafety,
7
+ MdScience,
8
+ MdFastfood,
9
+ } from "react-icons/md";
10
+ import { BiCameraMovie, BiMoviePlay } from "react-icons/bi";
11
+ import { GiBrain } from "react-icons/gi";
12
+ import { FaSpaceAwesome } from "react-icons/fa6";
13
+ import { BsCodeSlash } from "react-icons/bs";
14
+ import { FaUserTie } from "react-icons/fa";
15
+
16
+ export const sidebarData = [
17
+ { name: "Trending", icon: <AiTwotoneFire /> },
18
+ { name: "Adveture and Movies", icon: <BiMoviePlay /> },
19
+ { name: "Programming and Coding", icon: <BsCodeSlash /> },
20
+ { name: "Science and Fact", icon: <MdScience /> },
21
+ { name: "Technology and Space", icon: <FaSpaceAwesome /> },
22
+ { name: "Business and Startups", icon: <FaUserTie /> },
23
+ { name: "Sports and Highlightes", icon: <MdSportsBaseball /> },
24
+ { name: "Movies and Webseries", icon: <BiCameraMovie /> },
25
+ { name: "AI and ChatGpt", icon: <GiBrain /> },
26
+ { name: "Health and Fitness", icon: <MdOutlineHealthAndSafety /> },
27
+ { name: "Food and Blogging", icon: <MdFastfood /> },
28
+ { name: "Gaming and Live", icon: <MdSportsEsports /> },
29
+ ];
30
+
31
+ export const countries = [
32
+ {
33
+ name: "India",
34
+ countryCode: "IN",
35
+ url: "https://t4.ftcdn.net/jpg/02/81/47/57/240_F_281475718_rlQONmoS2E3CJtv0zFv2HwZ1weGhxpff.jpg",
36
+ },
37
+ {
38
+ name: "America",
39
+ countryCode: "US",
40
+ url: "https://t3.ftcdn.net/jpg/02/70/24/98/240_F_270249859_mf1Kyad7MO3Gb1BGvBahbB9SNttnVZO7.jpg",
41
+ },
42
+ {
43
+ name: "Germany",
44
+ countryCode: "DE",
45
+ url: "https://t3.ftcdn.net/jpg/04/44/28/64/240_F_444286454_6FR1VrzfVE8AJCwd28ft9T4pxEwH22Ng.jpg",
46
+ },
47
+ {
48
+ name: "Japan",
49
+ countryCode: "JP",
50
+ url: "https://t3.ftcdn.net/jpg/01/79/73/80/240_F_179738020_0cdBcea7tUpPoFTCiiVfl6p9chD28tQz.jpg",
51
+ },
52
+ {
53
+ name: "Canada",
54
+ countryCode: "CA",
55
+ url: "https://t3.ftcdn.net/jpg/01/71/57/72/240_F_171577280_Gj1SV9BV1vrvowWTexaiJW7OBj7uNgCT.jpg",
56
+ },
57
+ {
58
+ name: "England",
59
+ countryCode: "GB",
60
+ url: "https://t3.ftcdn.net/jpg/06/01/92/56/240_F_601925600_OPd3C0QuEE283YX2Fj6v3QtFFnkdtETF.jpg",
61
+ },
62
+ ];
src/components/layout/Header.jsx ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Avatar,
3
+ Box,
4
+ Flex,
5
+ Image,
6
+ Menu,
7
+ MenuButton,
8
+ MenuItem,
9
+ MenuList,
10
+ Portal,
11
+ Text,
12
+ } from "@chakra-ui/react";
13
+ import React, { useContext, useState } from "react";
14
+ import { NavLink } from "react-router-dom";
15
+ import { BsBell, BsCameraVideo } from "react-icons/bs";
16
+ import { MdOutlineCastConnected } from "react-icons/md";
17
+ import { BiSearch } from "react-icons/bi";
18
+ import { IoCompassOutline } from "react-icons/io5";
19
+
20
+ import { countries, sidebarData } from "../constants/Constants";
21
+ import YoutubeContext from "../../context/YoutubeContext";
22
+ import SearchBox from "./SearchBox";
23
+
24
+ const Header = () => {
25
+ const { setCountry, country } = useContext(YoutubeContext);
26
+ const [isOpen, setIsOpen] = useState(false);
27
+
28
+ const [flag, setFlag] = useState(
29
+ "https://t4.ftcdn.net/jpg/02/81/47/57/240_F_281475718_rlQONmoS2E3CJtv0zFv2HwZ1weGhxpff.jpg"
30
+ );
31
+ return (
32
+ <>
33
+ <Box zIndex={10} position={"sticky"} top={0}>
34
+ <Box
35
+ borderBottomWidth="1px"
36
+ borderColor={"#303030"}
37
+ padding={{ base: "0px 10px", sm: "0px 10px", md: "8px 35px" }}
38
+ borderStyle={"solid"}
39
+ bg={"#0f0f0f"}
40
+ >
41
+ <Flex justifyContent={"space-between"} alignItems={"center"}>
42
+ <NavLink to="/">
43
+ <Flex gap={""} alignItems={"center"}>
44
+ <Image
45
+ height={"7vh"}
46
+ objectFit={"cover"}
47
+ src={"/logo512.png"}
48
+ alt="logo"
49
+ />
50
+ <Text color={"white"} fontWeight={"bold"}>
51
+ YouTube
52
+ </Text>
53
+ </Flex>
54
+ </NavLink>
55
+
56
+ <Box>
57
+ <SearchBox />
58
+ </Box>
59
+
60
+ <Box
61
+ display={"flex"}
62
+ gap={{ base: 4, sm: 4, md: 6 }}
63
+ alignItems={"center"}
64
+ >
65
+ <Text
66
+ display={{ base: "none", sm: "none", md: "block" }}
67
+ fontSize={"xl"}
68
+ color={"white"}
69
+ >
70
+ <BsCameraVideo />
71
+ </Text>
72
+ <Text
73
+ display={{ base: "block", sm: "block", md: "none" }}
74
+ fontSize={"xl"}
75
+ color={"white"}
76
+ >
77
+ <MdOutlineCastConnected />
78
+ </Text>
79
+ <Text fontSize={"xl"} color={"white"}>
80
+ <BsBell />
81
+ </Text>
82
+ <Text
83
+ display={{ base: "block", sm: "block", md: "none" }}
84
+ fontSize={"xl"}
85
+ color={"white"}
86
+ _hover={{ cursor: "pointer" }}
87
+ >
88
+ <NavLink to="/search/mobile">
89
+ <BiSearch />
90
+ </NavLink>
91
+ </Text>
92
+ <Menu>
93
+ <MenuButton>
94
+ <Avatar
95
+ display={{ base: "none", sm: "none", md: "block" }}
96
+ size={"xs"}
97
+ name="Coutry"
98
+ src={flag}
99
+ />
100
+ </MenuButton>
101
+ <Portal>
102
+ <MenuList zIndex={11} bg={"#323232"}>
103
+ {countries.map((country) => (
104
+ <MenuItem
105
+ bg={"#323232"}
106
+ _hover={{ bg: "#5b5b5b" }}
107
+ color={"white"}
108
+ onClick={() => {
109
+ setFlag(country.url);
110
+ setCountry(country.countryCode);
111
+ }}
112
+ key={country.name}
113
+ >
114
+ {country.name}
115
+ </MenuItem>
116
+ ))}
117
+ </MenuList>
118
+ </Portal>
119
+ </Menu>
120
+ <Avatar
121
+ display={{ base: "block", sm: "block", md: "none" }}
122
+ size={"xs"}
123
+ name="Avatar"
124
+ src={"https://cdn-icons-png.flaticon.com/128/2202/2202112.png"}
125
+ />
126
+ </Box>
127
+ </Flex>
128
+ </Box>
129
+
130
+ <Box
131
+ bg={"#0f0f0f"}
132
+ display={{ base: "block", sm: "block", md: "none" }}
133
+ >
134
+ <Flex
135
+ className="hidden-scrollbar"
136
+ width={"100%"}
137
+ overflow={"scroll"}
138
+ align={"center"}
139
+ size={"xs"}
140
+ padding={"5px 10px"}
141
+ gap={2}
142
+ >
143
+ <Box
144
+ color="white"
145
+ bg={"#303030"}
146
+ _hover={{ bg: "#424242", cursor: "pointer" }}
147
+ _active={{ bg: "#ededed", color: "black" }}
148
+ fontSize={"2xl"}
149
+ padding={"4px 8px"}
150
+ borderRadius={"4px"}
151
+ onClick={() => setIsOpen(!isOpen)}
152
+ >
153
+ <IoCompassOutline />
154
+ </Box>
155
+ {countries.map((country_) => (
156
+ <Box
157
+ onClick={() => setCountry(country_.countryCode)}
158
+ key={country_.name}
159
+ _hover={{ cursor: "pointer" }}
160
+ color={country === country_.countryCode ? "black" : "white"}
161
+ bg={country === country_.countryCode ? "#ededed" : "#303030"}
162
+ padding={"4px 8px"}
163
+ borderRadius={"4px"}
164
+ >
165
+ {country_.name}
166
+ </Box>
167
+ ))}
168
+ </Flex>
169
+ </Box>
170
+ </Box>
171
+
172
+ <MobileMenubar isOpen={isOpen} setIsOpen={setIsOpen} />
173
+ </>
174
+ );
175
+ };
176
+
177
+ export default Header;
178
+
179
+ const MobileMenubar = ({ isOpen, setIsOpen }) => (
180
+ <>
181
+ <Box
182
+ width={"100vh"}
183
+ height={"100vw"}
184
+ position={"fixed"}
185
+ top={0}
186
+ left={0}
187
+ display={isOpen ? "Block" : "none"}
188
+ bg={"#0000009b"}
189
+ onClick={() => setIsOpen(!isOpen)}
190
+ zIndex={120}
191
+ >
192
+ <Box
193
+ bg="#303030"
194
+ height="100vh"
195
+ transform={isOpen ? "" : "translateX(-100%)"}
196
+ width={"80vw"}
197
+ transition={"all 1s"}
198
+ display={isOpen ? "Block" : "none"}
199
+ boxShadow="0px -1px 10px rgba(0, 0, 0, 0.1)"
200
+ onClick={() => setIsOpen(!isOpen)}
201
+ >
202
+ <Box>
203
+ <Box marginBottom={"15px"} padding={"0px 10px"}>
204
+ <NavLink to="/">
205
+ <Flex alignItems={"center"}>
206
+ <Image
207
+ height={"7vh"}
208
+ objectFit={"cover"}
209
+ src={"/logo512.png"}
210
+ alt="logo"
211
+ />
212
+ <Text color={"white"} fontWeight={"bold"}>
213
+ YouTube
214
+ </Text>
215
+ </Flex>
216
+ </NavLink>
217
+ </Box>
218
+
219
+ <Flex direction={"column"} gap="2">
220
+ {sidebarData.map((data) => (
221
+ <NavLink
222
+ key={data.name}
223
+ to={data.name === "Trending" ? "/" : `/?query=${data.name}`}
224
+ >
225
+ <Box
226
+ color="white"
227
+ display="flex"
228
+ gap={4}
229
+ padding="8px 20px"
230
+ alignItems="center"
231
+ _hover={{ background: "#3a3a3a" }}
232
+ >
233
+ {" "}
234
+ <Text fontSize="2xl">{data.icon} </Text>{" "}
235
+ <Text>{data.name}</Text>
236
+ </Box>
237
+ </NavLink>
238
+ ))}
239
+ </Flex>
240
+ </Box>
241
+ </Box>
242
+ </Box>
243
+ </>
244
+ );
src/components/layout/HomeSkeleton.jsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Box, Flex, Skeleton, SkeletonCircle, Text } from "@chakra-ui/react";
3
+
4
+ const HomeSkeleton = () => (
5
+ <>
6
+ <Box width={"100%"}>
7
+ <Skeleton>
8
+ <Box
9
+ borderRadius={"8px"}
10
+ position={"relative"}
11
+ backgroundRepeat={"no-repeat"}
12
+ height={"180px"}
13
+ backgroundImage={"url('https://i.ytimg.com/vi/-ME4gY9i4G4/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDlivhEse-sboxfW57u_wU_4KLAGQ')"}
14
+ backgroundSize={"cover"}
15
+ backgroundColor="#2e2c2c"
16
+ backgroundPosition={"center"}
17
+ ></Box>
18
+ </Skeleton>
19
+
20
+ <Flex marginTop={"10px"} gap={3}>
21
+ <SkeletonCircle>
22
+ <Box height={"35px"} borderRadius={"100%"} width={"35px"} />
23
+ </SkeletonCircle>
24
+
25
+ <Box color={"white"}>
26
+ <Skeleton>
27
+ <Text fontWeight={"semibold"}>
28
+ {
29
+ "This Is Video Home Title - This Is Video Home Title - This Is Video Home Title"
30
+ }
31
+ </Text>
32
+ </Skeleton>
33
+ <Skeleton>
34
+ <Text marginTop={"5px"} fontSize={"xs"}>
35
+ {"channelName"}
36
+ </Text>
37
+ </Skeleton>
38
+ <Skeleton>
39
+ <Text fontSize={"xs"}>20 M views 1 year ago</Text>
40
+ </Skeleton>
41
+ </Box>
42
+ </Flex>
43
+ </Box>
44
+ </>
45
+ );
46
+
47
+ export default HomeSkeleton;
src/components/layout/Layout.jsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Box, Flex } from "@chakra-ui/react";
3
+
4
+ import Header from "./Header";
5
+ import Sidebar from "./Sidebar";
6
+
7
+ const Layout = ({ children }) => (
8
+ <>
9
+ <Box bg={"#0f0f0f"}>
10
+ <Header />
11
+
12
+ <Flex>
13
+ <Box
14
+ display={{ base: "none", sm: "none", md: "block" }}
15
+ flexShrink={0}
16
+ width={"280px"}
17
+ >
18
+ <Box
19
+ position={"fixed"}
20
+ top={"10vh"}
21
+ left={"0"}
22
+ minWidth={"fit-content"}
23
+ >
24
+ <Sidebar />
25
+ </Box>
26
+ </Box>
27
+
28
+ <Box>
29
+ <Box minHeight={"90vh"} width={"100%"} margin={"auto"}>
30
+ {children}
31
+ </Box>
32
+ </Box>
33
+ </Flex>
34
+ </Box>
35
+ </>
36
+ );
37
+
38
+ export default Layout;
src/components/layout/MobileSearch.jsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useContext, useEffect, useState } from "react";
2
+ import {
3
+ Box,
4
+ Flex,
5
+ IconButton,
6
+ Input,
7
+ InputGroup,
8
+ Text,
9
+ } from "@chakra-ui/react";
10
+ import { BiArrowBack, BiSearch } from "react-icons/bi";
11
+ import { MdKeyboardVoice } from "react-icons/md";
12
+ import { NavLink, useNavigate } from "react-router-dom";
13
+ import { debounce } from "lodash";
14
+
15
+ import YoutubeContext from "../../context/YoutubeContext";
16
+
17
+ const MobileSearch = () => {
18
+ const { generateAutocomplete, autocomplete } = useContext(YoutubeContext);
19
+ const [searchText, setSearchText] = useState("");
20
+
21
+ const generateAutocompleteFun = useCallback(
22
+ debounce((query) => {
23
+ generateAutocomplete(query);
24
+ }, 500),
25
+ []
26
+ );
27
+
28
+ const navigate = useNavigate();
29
+
30
+ const searchFun = () => {
31
+ navigate(`/?query=${searchText}`);
32
+ };
33
+
34
+ useEffect(() => {
35
+ window.scrollTo(0, 0);
36
+ }, []);
37
+
38
+ return (
39
+ <>
40
+ <Box width={"100%"} minHeight={"100vh"} bg={"#0f0f0f"}>
41
+ <Flex padding={"8px 15px"} align={"center"} gap={4}>
42
+ <Text fontSize={"xl"} _hover={{ cursor: "pointer" }}>
43
+ <NavLink to="/">
44
+ <BiArrowBack color="white" />
45
+ </NavLink>
46
+ </Text>
47
+ <InputGroup>
48
+ <Input
49
+ border={"none"}
50
+ color={"white"}
51
+ placeholder="Search YouTube"
52
+ borderRadius={"30px"}
53
+ bg={"#303030"}
54
+ height={"fit-content"}
55
+ padding={"5px 15px"}
56
+ _focusVisible={false}
57
+ value={searchText || ""}
58
+ onChange={(e) => {
59
+ setSearchText(e.target.value);
60
+ generateAutocompleteFun(e.target.value);
61
+ }}
62
+ onKeyDown={(event) => {
63
+ if (event.key === "Enter") {
64
+ searchFun();
65
+ }
66
+ }}
67
+ />
68
+ </InputGroup>
69
+ {!searchText && (
70
+ <IconButton
71
+ size={"sm"}
72
+ bg={"#303030"}
73
+ _hover={{ bg: "#424242" }}
74
+ borderRadius="100%"
75
+ aria-label="Mice"
76
+ icon={<MdKeyboardVoice color="white" size={"24px"} />}
77
+ />
78
+ )}
79
+ </Flex>
80
+
81
+ <AutoSuggetionMobile autocomplete={autocomplete} />
82
+ </Box>
83
+ </>
84
+ );
85
+ };
86
+
87
+ export default MobileSearch;
88
+
89
+ const AutoSuggetionMobile = ({ autocomplete }) => (
90
+ <>
91
+ <Flex direction={"column"} gap={2} marginTop={"15px"}>
92
+ {autocomplete &&
93
+ autocomplete.slice(0, 9).map((text) => (
94
+ <Box key={text} _hover={{ bg: "#3a3a3a" }}>
95
+ <NavLink to={`/?query=${text}`}>
96
+ <Flex padding={"10px 15px"} align={"center"} gap={5}>
97
+ <Text fontSize={"xl"}>
98
+ <BiSearch color={"white"} />
99
+ </Text>
100
+ <Text color={"white"}>{text}</Text>
101
+ </Flex>
102
+ </NavLink>
103
+ </Box>
104
+ ))}
105
+ </Flex>
106
+ </>
107
+ );
src/components/layout/MobileSidebar.jsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // import React from "react";
2
+
3
+ // const MobileSidebar = () => {
4
+ // return
5
+ // <>
6
+ // <Box>
7
+
8
+ // </Box>
9
+ // </>;
10
+ // };
11
+
12
+ // export default MobileSidebar;
src/components/layout/SearchBox.jsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Box,
3
+ IconButton,
4
+ Input,
5
+ InputGroup,
6
+ InputLeftElement,
7
+ Text,
8
+ } from "@chakra-ui/react";
9
+ import React, {
10
+ useCallback,
11
+ useContext,
12
+ useEffect,
13
+ useRef,
14
+ useState,
15
+ } from "react";
16
+ import { BiSearch } from "react-icons/bi";
17
+ import { MdKeyboardVoice } from "react-icons/md";
18
+ import { NavLink, useLocation, useNavigate } from "react-router-dom";
19
+ import { debounce } from "lodash";
20
+
21
+ import YoutubeContext from "../../context/YoutubeContext";
22
+
23
+ const SearchBox = () => {
24
+ const { generateAutocomplete, autocomplete } = useContext(YoutubeContext);
25
+ const [searchText, setSearchText] = useState("");
26
+ const [showSuggestion, setShowSuggestion] = useState(false);
27
+ const inputRef = useRef();
28
+
29
+ const generateAutocompleteFun = useCallback(
30
+ debounce((query) => {
31
+ generateAutocomplete(query);
32
+ }, 500),
33
+ []
34
+ );
35
+
36
+ useEffect(() => {
37
+ const handleMouseDown = (event) => {
38
+ if (event.target?.parentElement?.tagName === "A") return;
39
+ if (inputRef.current && !inputRef.current.contains(event.target)) {
40
+ setShowSuggestion(false);
41
+ }
42
+ };
43
+
44
+ const handleFocusIn = (event) => {
45
+ if (inputRef.current === event.target) {
46
+ setShowSuggestion(true);
47
+ } else {
48
+ // setShowSuggestion(false);
49
+ }
50
+ };
51
+
52
+ window.addEventListener("mousedown", handleMouseDown);
53
+ window.addEventListener("focusin", handleFocusIn);
54
+ });
55
+
56
+ // Access query parameters
57
+ const location = useLocation();
58
+ const queryParams = new URLSearchParams(location.search);
59
+
60
+ const query = queryParams.get("query");
61
+
62
+ useEffect(() => {
63
+ setSearchText(query);
64
+ setShowSuggestion(false);
65
+ }, [query]);
66
+
67
+ const navigate = useNavigate();
68
+
69
+ const searchFun = () => {
70
+ navigate(`/?query=${searchText}`);
71
+ };
72
+
73
+ return (
74
+ <>
75
+ <Box
76
+ display={{ base: "none", sm: "none", md: "flex" }}
77
+ alignItems={"center"}
78
+ gap={6}
79
+ >
80
+ <Box position={"relative"}>
81
+ <Box display={"flex"} alignItems="center" width={"500px"}>
82
+ <InputGroup>
83
+ <InputLeftElement pointerEvents="none">
84
+ <BiSearch size={"20px"} color="white" />
85
+ </InputLeftElement>
86
+ <Input
87
+ ref={inputRef}
88
+ focusBorderColor="#7373ff"
89
+ borderColor={"#303030"}
90
+ display={"flex"}
91
+ alignItems={"center"}
92
+ color={"white"}
93
+ placeholder="Search"
94
+ borderRadius={"30px 0 0 30px"}
95
+ fontSize={"lg"}
96
+ value={searchText || ""}
97
+ onChange={(e) => {
98
+ setSearchText(e.target.value);
99
+ generateAutocompleteFun(e.target.value);
100
+ }}
101
+ onKeyDown={(event) => {
102
+ if (event.key === "Enter") {
103
+ searchFun();
104
+ setShowSuggestion(false);
105
+ }
106
+ }}
107
+ />
108
+ </InputGroup>
109
+ <IconButton
110
+ borderColor={"#303030"}
111
+ borderWidth={"1px 1px 1px 0"}
112
+ borderStyle={"solid"}
113
+ borderRadius={"0 30px 30px 0"}
114
+ bg={"#303030"}
115
+ _hover={{ bg: "#424242" }}
116
+ icon={<BiSearch size={"24px"} color="white" />}
117
+ onClick={searchFun}
118
+ />
119
+ </Box>
120
+ <Box
121
+ position={"absolute"}
122
+ top={"45px"}
123
+ width="460px"
124
+ hidden={!showSuggestion}
125
+ >
126
+ <AutoSuggestion autocomplete={autocomplete} />
127
+ </Box>
128
+ </Box>
129
+ <Box>
130
+ <IconButton
131
+ bg={"#303030"}
132
+ _hover={{ bg: "#424242" }}
133
+ borderRadius="100%"
134
+ aria-label="Mice"
135
+ icon={<MdKeyboardVoice color="white" size={"24px"} />}
136
+ />
137
+ </Box>
138
+ </Box>
139
+ </>
140
+ );
141
+ };
142
+
143
+ export default SearchBox;
144
+
145
+ const AutoSuggestion = ({ autocomplete }) => (
146
+ <>
147
+ {autocomplete && autocomplete.length !== 0 && (
148
+ <Box bg="#222222" borderRadius={"10px"} padding={"15px 0"}>
149
+ {autocomplete.map((text) => (
150
+ <NavLink to={`/?query=${text}`} key={text}>
151
+ <Text
152
+ display={"flex"}
153
+ gap={4}
154
+ alignItems={"center"}
155
+ color="white"
156
+ _hover={{ bg: "#3a3a3a" }}
157
+ padding={"3px 10px"}
158
+ fontSize={"lg"}
159
+ >
160
+ <BiSearch size={"20px"} color="white" />
161
+ {text}
162
+ </Text>
163
+ </NavLink>
164
+ ))}
165
+ </Box>
166
+ )}
167
+ </>
168
+ );
src/components/layout/SearchSkeleton.jsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Box,
3
+ Flex,
4
+ Image,
5
+ Skeleton,
6
+ SkeletonCircle,
7
+ Text,
8
+ } from "@chakra-ui/react";
9
+ import React from "react";
10
+
11
+ const SearchSkeleton = () => (
12
+ <>
13
+ <Flex width={"100%"} gap={5}>
14
+ <Skeleton>
15
+ <Box
16
+ backgroundColor="#2e2c2c"
17
+ borderRadius={"8px"}
18
+ position={"relative"}
19
+ overflow={"hidden"}
20
+ width={"310px"}
21
+ height={"170px"}
22
+ >
23
+ <Image
24
+ src={"'https://i.ytimg.com/vi/-ME4gY9i4G4/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDlivhEse-sboxfW57u_wU_4KLAGQ"}
25
+ height="100%"
26
+ width={"100%"}
27
+ alt="thumbnail"
28
+ objectFit={"cover"}
29
+ />
30
+ </Box>
31
+ </Skeleton>
32
+
33
+ <Box width="70%">
34
+ <Skeleton>
35
+ {" "}
36
+ <Text fontSize={"2xl"} color={"white"}>
37
+ This is title for a Video, This is title for a Video, This is
38
+ </Text>
39
+ <Text fontSize={"2xl"} color={"white"}>
40
+ This is title Line
41
+ </Text>
42
+ </Skeleton>
43
+ <Skeleton>
44
+ <Text marginTop={"5px"} color={"#b7b5b5"} fontSize={"sm"}>
45
+ 2m views 1 day ago
46
+ </Text>
47
+ </Skeleton>
48
+
49
+ <Flex marginTop={"10px"} gap={3} alignItems={"center"}>
50
+ <SkeletonCircle>
51
+ <Image
52
+ height={"35px"}
53
+ borderRadius={"100%"}
54
+ src={"avatar"}
55
+ alt="channel"
56
+ />
57
+ </SkeletonCircle>
58
+
59
+ <Skeleton>
60
+ <Text color={"#b7b5b5"} fontSize={"sm"}>
61
+ channel name
62
+ </Text>
63
+ </Skeleton>
64
+ </Flex>
65
+ </Box>
66
+ </Flex>
67
+ </>
68
+ );
69
+
70
+ export default SearchSkeleton;
src/components/layout/Sidebar.jsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { NavLink } from "react-router-dom";
3
+ import { Box, Flex, Text } from "@chakra-ui/react";
4
+
5
+ import { sidebarData } from "../constants/Constants";
6
+
7
+ const Sidebar = () => (
8
+ <>
9
+ <Flex
10
+ width={"fit-content"}
11
+ padding={"20px"}
12
+ direction={"column"}
13
+ height={"90vh"}
14
+ gap={2}
15
+ position={"relative"}
16
+ left={0}
17
+ >
18
+ {sidebarData.map((data) => (
19
+ <NavLink
20
+ key={data.name}
21
+ to={data.name === "Trending" ? "/" : `/?query=${data.name}`}
22
+ >
23
+ <Box
24
+ color="white"
25
+ display="flex"
26
+ gap={4}
27
+ borderRadius="5px"
28
+ padding="5px"
29
+ alignItems="center"
30
+ zIndex={10}
31
+ _hover={{ background: "#3a3a3a" }}
32
+ >
33
+ {" "}
34
+ <Text fontSize="2xl">{data.icon} </Text> <Text>{data.name}</Text>
35
+ </Box>
36
+ </NavLink>
37
+ ))}
38
+ </Flex>
39
+ </>
40
+ );
41
+
42
+ export default Sidebar;
src/components/list/RelatedList.jsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useContext, useEffect } from "react";
2
+ import { Grid, Box } from "@chakra-ui/react";
3
+ import numeral from "numeral";
4
+ import { formatDistanceToNow } from "date-fns";
5
+
6
+ import RelatedVideoCard from "../cards/RelatedVideoCard";
7
+ import HomeVideoCard from "../cards/HomeVideoCard";
8
+ import YoutubeContext from "../../context/YoutubeContext";
9
+
10
+ const RelatedList = ({ videoId }) => {
11
+ const { trendingVideos, country, getTrendingVideos } =
12
+ useContext(YoutubeContext);
13
+
14
+ useEffect(() => {
15
+ window.scrollTo(0, 0);
16
+ getTrendingVideos();
17
+ }, [country]);
18
+
19
+ return (
20
+ <>
21
+ <Box display={{ base: "none", sm: "none", md: "block" }}>
22
+ <Grid gap={5} padding={{ base: "8px", md: "0px" }}>
23
+ {trendingVideos &&
24
+ trendingVideos
25
+ .filter((video) => video.id !== videoId)
26
+ .map((video) => (
27
+ <RelatedVideoCard
28
+ videoId={video.id}
29
+ channelId={video?.snippet?.channelId}
30
+ duration={
31
+ video.contentDetails?.duration
32
+ ? durationConverter(video.contentDetails?.duration)
33
+ : ""
34
+ }
35
+ key={video.id}
36
+ title={
37
+ video.snippet?.title
38
+ ? formateTitle(convertHtmlEntities(video.snippet.title))
39
+ : ""
40
+ }
41
+ thumbnail={
42
+ video?.snippet.thumbnails?.maxres?.url ||
43
+ video?.snippet.thumbnails?.standard?.url ||
44
+ ""
45
+ }
46
+ postTime={timeConverter(video.snippet.publishedAt)}
47
+ views={viewsConverter(video.statistics.viewCount)}
48
+ channelName={video.snippet.channelTitle}
49
+ />
50
+ ))}
51
+ </Grid>
52
+ </Box>
53
+ <Box display={{ base: "block", sm: "block", md: "none" }}>
54
+ <Grid gap={5} padding={{ base: "8px", md: "0px" }}>
55
+ {trendingVideos &&
56
+ trendingVideos
57
+ .filter((video) => video.id !== videoId)
58
+ .map((video) => (
59
+ <HomeVideoCard
60
+ videoId={video.id}
61
+ channelId={video?.snippet?.channelId}
62
+ duration={
63
+ video.contentDetails?.duration
64
+ ? durationConverter(video.contentDetails?.duration)
65
+ : ""
66
+ }
67
+ key={video.id}
68
+ title={
69
+ video.snippet?.title
70
+ ? formateTitle(convertHtmlEntities(video.snippet.title))
71
+ : ""
72
+ }
73
+ thumbnail={
74
+ video?.snippet.thumbnails?.maxres?.url ||
75
+ video?.snippet.thumbnails?.standard?.url ||
76
+ ""
77
+ }
78
+ postTime={timeConverter(video.snippet.publishedAt)}
79
+ views={viewsConverter(video.statistics.viewCount)}
80
+ channelName={video.snippet.channelTitle}
81
+ />
82
+ ))}
83
+ </Grid>
84
+ </Box>
85
+ </>
86
+ );
87
+ };
88
+
89
+ export default RelatedList;
90
+
91
+ // Views
92
+ const viewsConverter = (views) => {
93
+ const formattedViews = numeral(views).format("0.[00]a");
94
+
95
+ return formattedViews;
96
+ };
97
+
98
+ // Convert HTML entities in title
99
+ function convertHtmlEntities(inputString) {
100
+ const textarea = document.createElement("textarea");
101
+ textarea.innerHTML = inputString;
102
+ return textarea.value;
103
+ }
104
+
105
+ // Video Duration
106
+ const durationConverter = (duration) => {
107
+ const matches = duration.match(/PT(?:(\d+)M)?(\d+)S/);
108
+ if (!matches) return "";
109
+
110
+ const minutes = parseInt(matches[1]) || 0;
111
+ const seconds = parseInt(matches[2]) || 0;
112
+
113
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
114
+ };
115
+
116
+ // Video Uploaded
117
+ const timeConverter = (time) => {
118
+ const date = new Date(time);
119
+ return formatDistanceToNow(date, { addSuffix: true });
120
+ };
121
+
122
+ // Video Title
123
+ const formateTitle = (title) => {
124
+ const char = title.split("");
125
+
126
+ if (char.length < 60) {
127
+ return title;
128
+ }
129
+
130
+ return `${char.slice(0, 60).join("")}...`;
131
+ };
src/components/list/SearchList.jsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Fragment, useContext, useEffect } from "react";
2
+ import { Grid } from "@chakra-ui/react";
3
+ import { useLocation } from "react-router-dom";
4
+ import { formatDistanceToNow } from "date-fns";
5
+
6
+ import YoutubeContext from "../../context/YoutubeContext";
7
+ import HomeVideoCard from "../cards/HomeVideoCard";
8
+ import HomeSkeleton from "../layout/HomeSkeleton";
9
+
10
+ const SearchVideoList = () => {
11
+ const { getSearchVideos, searchVideos, isLoading, country } =
12
+ useContext(YoutubeContext);
13
+
14
+ const location = useLocation();
15
+ const queryParams = new URLSearchParams(location.search);
16
+
17
+ // Access query parameters
18
+ const query = queryParams.get("query");
19
+
20
+ useEffect(() => {
21
+ window.scrollTo(0, 0);
22
+ getSearchVideos(query);
23
+ }, [query, country]);
24
+
25
+ return (
26
+ <Fragment>
27
+ <Grid
28
+ gridTemplateColumns={{
29
+ base: "1fr",
30
+ sm: "1fr",
31
+ md: "1fr 1fr",
32
+ lg: "1fr 1fr 1fr",
33
+ }}
34
+ gap={5}
35
+ width="100%"
36
+ padding={{ base: "0px", sm: "0px", md: "30px" }}
37
+ paddingTop={"5px"}
38
+ >
39
+ {isLoading ? (
40
+ <>
41
+ <HomeSkeleton />
42
+ <HomeSkeleton />
43
+ <HomeSkeleton />
44
+ <HomeSkeleton />
45
+ <HomeSkeleton />
46
+ <HomeSkeleton />
47
+ </>
48
+ ) : (
49
+ ""
50
+ )}
51
+ {searchVideos &&
52
+ searchVideos.map((video) => (
53
+ <HomeVideoCard
54
+ key={video.id.videoId}
55
+ videoId={video.id.videoId}
56
+ channelId={video?.snippet?.channelId}
57
+ duration={"04:35"}
58
+ title={formateTitle(convertHtmlEntities(video.snippet.title))}
59
+ thumbnail={video.snippet.thumbnails.high.url}
60
+ avatar={""}
61
+ postTime={timeConverter(video.snippet.publishedAt)}
62
+ views={""}
63
+ channelName={video.snippet.channelTitle}
64
+ />
65
+ ))}
66
+ <>
67
+ <HomeSkeleton />
68
+ <HomeSkeleton />
69
+ <HomeSkeleton />
70
+ <HomeSkeleton />
71
+ <HomeSkeleton />
72
+ <HomeSkeleton />
73
+ </>
74
+ </Grid>
75
+ </Fragment>
76
+ );
77
+ };
78
+
79
+ export default SearchVideoList;
80
+
81
+ // Video Uploaded Time,
82
+ const timeConverter = (time) => {
83
+ const date = new Date(time);
84
+ return formatDistanceToNow(date, { addSuffix: true });
85
+ };
86
+
87
+ // Convert HTML entities in title
88
+ function convertHtmlEntities(inputString) {
89
+ const textarea = document.createElement("textarea");
90
+ textarea.innerHTML = inputString;
91
+ return textarea.value;
92
+ }
93
+
94
+ // Video Title
95
+ const formateTitle = (title) => {
96
+ const char = title.split("");
97
+
98
+ if (char.length < 60) {
99
+ return title;
100
+ }
101
+
102
+ return `${char.slice(0, 60).join("")}...`;
103
+ };
src/components/list/TrendingList.jsx ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useContext, useEffect } from "react";
2
+ import { Box, Grid, Text } from "@chakra-ui/react";
3
+ import { formatDistanceToNow } from "date-fns";
4
+ import numeral from "numeral";
5
+ import InfiniteScroll from "react-infinite-scroll-component";
6
+ import axios from "axios";
7
+
8
+ import YoutubeContext from "../../context/YoutubeContext";
9
+ import SearchSkeleton from "../layout/SearchSkeleton";
10
+ import { errorHandling } from "../../utils/utils";
11
+ import SearchVideoCard from "../cards/SearchVideoCard";
12
+ import HomeSkeleton from "../layout/HomeSkeleton";
13
+ import HomeVideoCard from "../cards/HomeVideoCard";
14
+
15
+ const TrendingList = () => {
16
+ const {
17
+ trendingVideos,
18
+ country,
19
+ getTrendingVideos,
20
+ isLoading,
21
+ setIsLoading,
22
+ setTrendingVideos,
23
+ trendingVideosLength,
24
+ nextPageToken,
25
+ setNextPageToken,
26
+ } = useContext(YoutubeContext);
27
+
28
+ useEffect(() => {
29
+ window.scrollTo(0, 0);
30
+ getTrendingVideos();
31
+ }, [country]);
32
+
33
+ const fetchMoreData = async () => {
34
+ try {
35
+ setIsLoading(true);
36
+
37
+ console.log(nextPageToken);
38
+ const res2 = await axios.get(
39
+ `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&chart=mostPopular&regionCode=${country}&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE2}&maxResults=15&pageToken=${nextPageToken}`
40
+ );
41
+ setTrendingVideos((prevTrendingVideos) => [
42
+ ...prevTrendingVideos,
43
+ ...res2.data.items,
44
+ ]);
45
+ setIsLoading(false);
46
+
47
+ setNextPageToken(res2.data.nextPageToken);
48
+ } catch (error) {
49
+ try {
50
+ setIsLoading(true);
51
+
52
+ const res2 = await axios.get(
53
+ `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&chart=mostPopular&regionCode=${country}&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE1}&maxResults=15&pageToken=${nextPageToken}`
54
+ );
55
+ setTrendingVideos((prevTrendingVideos) => [
56
+ ...prevTrendingVideos,
57
+ ...res2.data.items,
58
+ ]);
59
+ setIsLoading(false);
60
+
61
+ setNextPageToken(res2.data.nextPageToken);
62
+ } catch (error) {
63
+ errorHandling(error);
64
+ }
65
+ }
66
+ };
67
+
68
+ return (
69
+ <>
70
+ <Box display={{ base: "none", sm: "none", md: "block" }}>
71
+ <InfiniteScroll
72
+ dataLength={trendingVideos.length} //This is important field to render the next data
73
+ next={fetchMoreData}
74
+ hasMore={
75
+ trendingVideosLength > trendingVideos.length + 1 ? true : false
76
+ }
77
+ loader={
78
+ isLoading ? (
79
+ <Grid gap={5} padding={"30px"}>
80
+ <>
81
+ <SearchSkeleton />
82
+ <SearchSkeleton />
83
+ <SearchSkeleton />
84
+ </>
85
+ </Grid>
86
+ ) : (
87
+ ""
88
+ )
89
+ }
90
+ endMessage={
91
+ <Text color={"white"} textAlign={"center"} padding={"20px"}>
92
+ <b>👍 Yay! You have seen it all</b>
93
+ </Text>
94
+ }
95
+ >
96
+ <>
97
+ <Grid gap={5} padding={"30px"}>
98
+ {trendingVideos &&
99
+ trendingVideos.map((video) => (
100
+ <SearchVideoCard
101
+ videoId={video.id}
102
+ channelId={video?.snippet?.channelId}
103
+ duration={
104
+ video.contentDetails?.duration
105
+ ? durationConverter(video.contentDetails?.duration)
106
+ : ""
107
+ }
108
+ key={video.id}
109
+ title={
110
+ video.snippet?.title
111
+ ? formateTitle(convertHtmlEntities(video.snippet.title))
112
+ : ""
113
+ }
114
+ thumbnail={
115
+ video?.snippet.thumbnails?.maxres?.url ||
116
+ video?.snippet.thumbnails?.standard?.url ||
117
+ ""
118
+ }
119
+ avatar={""}
120
+ postTime={timeConverter(video.snippet.publishedAt)}
121
+ views={viewsConverter(video.statistics.viewCount)}
122
+ channelName={video.snippet.channelTitle}
123
+ />
124
+ ))}
125
+ {trendingVideosLength > trendingVideos.length + 1 ? (
126
+ <>
127
+ <SearchSkeleton />
128
+ <SearchSkeleton />
129
+ <SearchSkeleton />
130
+ </>
131
+ ) : (
132
+ ""
133
+ )}
134
+ </Grid>
135
+ </>
136
+ </InfiniteScroll>
137
+ </Box>
138
+ <Box display={{ base: "block", sm: "block", md: "none" }}>
139
+ <InfiniteScroll
140
+ dataLength={trendingVideos.length} //This is important field to render the next data
141
+ // next={fetchMoreData}
142
+ hasMore={
143
+ trendingVideosLength > trendingVideos.length + 1 ? true : false
144
+ }
145
+ loader={
146
+ isLoading ? (
147
+ <Grid gap={5}>
148
+ <>
149
+ <HomeSkeleton />
150
+ <HomeSkeleton />
151
+ <HomeSkeleton />
152
+ </>
153
+ </Grid>
154
+ ) : (
155
+ ""
156
+ )
157
+ }
158
+ endMessage={
159
+ <Text color={"white"} textAlign={"center"} padding={"20px"}>
160
+ <b>👍 Yay! You have seen it all</b>
161
+ </Text>
162
+ }
163
+ >
164
+ <>
165
+ <Grid gap={5} paddingTop={"5px"}>
166
+ {trendingVideos &&
167
+ trendingVideos.map((video) => (
168
+ <HomeVideoCard
169
+ videoId={video.id}
170
+ channelId={video?.snippet?.channelId}
171
+ duration={
172
+ video.contentDetails?.duration
173
+ ? durationConverter(video.contentDetails?.duration)
174
+ : ""
175
+ }
176
+ key={video.id}
177
+ title={
178
+ video.snippet?.title
179
+ ? formateTitle(convertHtmlEntities(video.snippet.title))
180
+ : ""
181
+ }
182
+ thumbnail={
183
+ video?.snippet.thumbnails?.maxres?.url ||
184
+ video?.snippet.thumbnails?.standard?.url ||
185
+ ""
186
+ }
187
+ avatar={""}
188
+ postTime={timeConverter(video.snippet.publishedAt)}
189
+ views={viewsConverter(video.statistics.viewCount)}
190
+ channelName={video.snippet.channelTitle}
191
+ />
192
+ ))}
193
+ {trendingVideosLength > trendingVideos.length + 1 ? (
194
+ <>
195
+ <HomeSkeleton />
196
+ <HomeSkeleton />
197
+ <HomeSkeleton />
198
+ </>
199
+ ) : (
200
+ ""
201
+ )}
202
+ </Grid>
203
+ </>
204
+ </InfiniteScroll>
205
+ </Box>
206
+ </>
207
+ );
208
+ };
209
+
210
+ export default TrendingList;
211
+
212
+ // Views
213
+ const viewsConverter = (views) => {
214
+ const formattedViews = numeral(views).format("0.[00]a");
215
+
216
+ return formattedViews;
217
+ };
218
+
219
+ // Convert HTML entities in title
220
+ function convertHtmlEntities(inputString) {
221
+ const textarea = document.createElement("textarea");
222
+ textarea.innerHTML = inputString;
223
+ return textarea.value;
224
+ }
225
+
226
+ // Video Duration
227
+ const durationConverter = (duration) => {
228
+ const matches = duration.match(/PT(?:(\d+)M)?(\d+)S/);
229
+ if (!matches) return "";
230
+
231
+ const minutes = parseInt(matches[1]) || 0;
232
+ const seconds = parseInt(matches[2]) || 0;
233
+
234
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
235
+ };
236
+
237
+ // Video Uploaded
238
+ const timeConverter = (time) => {
239
+ const date = new Date(time);
240
+ return formatDistanceToNow(date, { addSuffix: true });
241
+ };
242
+
243
+ // Video Title
244
+ const formateTitle = (title) => {
245
+ const char = title.split("");
246
+
247
+ if (char.length < 100) {
248
+ return title;
249
+ }
250
+
251
+ return `${char.slice(0, 100).join("")}...`;
252
+ };
src/context/YoutubeContext.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useState } from "react";
2
+ import axios from "axios";
3
+
4
+ import { errorHandling } from "../utils/utils";
5
+
6
+ const YoutubeContext = createContext()
7
+ export default YoutubeContext
8
+
9
+ export const ContextProvider = ({ children }) => {
10
+ const [trendingVideos, setTrendingVideos] = useState([]);
11
+ const [searchVideos, setSearchVideos] = useState([]);
12
+ const [trendingVideosLength, setTrendingVideosLength] = useState(15);
13
+ const [autocomplete, setAutocomplete] = useState([]);
14
+ const [country, setCountry] = useState("IN");
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [nextPageToken, setNextPageToken] = useState("");
17
+
18
+ // 1. Autocomplete Suggetions
19
+ const generateAutocomplete = async (query) => {
20
+ try {
21
+ const res = await axios.get(`https://cors-handlers.vercel.app/api/?url=http%3A%2F%2Fsuggestqueries.google.com%2Fcomplete%2Fsearch%3Fclient%3Dfirefox%26ds%3Dyt%26q=${query}`)
22
+ //const str = await axios.get(`https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&num=10&q=${query}`)
23
+
24
+ //console.log(res.data[1].map((arr) => arr))
25
+ setAutocomplete(res.data[1].map((arr) => arr))
26
+ //const res = await str.text();
27
+ //console.log(JSON.parse(res.data.split(/\(|\)/)[1])[1].map((arr) => arr[0]))
28
+ //setAutocomplete(JSON.parse(res.data.split(/\(|\)/)[1])[1].map((arr) => arr[0]))
29
+
30
+ } catch (error) {
31
+ console.log(error)
32
+ const options = {
33
+
34
+ method: "GET",
35
+ url: "https://youtube-data8.p.rapidapi.com/auto-complete/",
36
+ params: {
37
+ q: query,
38
+ hl: "en",
39
+ gl: "US"
40
+ },
41
+ headers: {
42
+ "X-RapidAPI-Key": process.env.REACT_APP_YOUTUBE_API_KEY_RAPIDAPI,
43
+ "X-RapidAPI-Host": "youtube-data8.p.rapidapi.com"
44
+ }
45
+ };
46
+
47
+ try {
48
+ const response = await axios.request(options);
49
+ //console.log(response.data);
50
+ setAutocomplete(response?.data?.results)
51
+ } catch (error) {
52
+ errorHandling(error)
53
+ }
54
+ }
55
+ }
56
+
57
+ // 2. Trending Videos
58
+ const getTrendingVideos = async () => {
59
+ setIsLoading(true)
60
+ try {
61
+ const res = await axios.get(`https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&chart=mostPopular&regionCode=${country}&key=${`${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE2}`}&maxResults=15`)
62
+ setTrendingVideos(res.data.items)
63
+ setTrendingVideosLength(res.data.pageInfo.totalResults)
64
+ setNextPageToken(res.data.nextPageToken)
65
+ setIsLoading(false)
66
+ } catch (error) {
67
+
68
+ try {
69
+ const res = await axios.get(`https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&chart=mostPopular&regionCode=${country}&key=${`${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE1}`}&maxResults=15`)
70
+ setTrendingVideos(res.data.items)
71
+ setTrendingVideosLength(res.data.pageInfo.totalResults)
72
+ setNextPageToken(res.data.nextPageToken)
73
+ setIsLoading(false)
74
+ } catch (error) {
75
+ errorHandling(error)
76
+ }
77
+
78
+ }
79
+ }
80
+
81
+ // 3. Search videos
82
+ const getSearchVideos = async (query) => {
83
+ setIsLoading(true)
84
+
85
+ const options = {
86
+ method: "GET",
87
+ url: "https://youtube-v31.p.rapidapi.com/search",
88
+ params: {
89
+ q: query,
90
+ part: "snippet,id",
91
+ regionCode: country,
92
+ maxResults: "50",
93
+ type: "video",
94
+ videoDuration: "medium"
95
+ },
96
+ headers: {
97
+ "X-RapidAPI-Key": `${process.env.REACT_APP_YOUTUBE_API_KEY_RAPIDAPI}`,
98
+ "X-RapidAPI-Host": "youtube-v31.p.rapidapi.com"
99
+ }
100
+ };
101
+
102
+ try {
103
+ const response = await axios.request(options);
104
+ console.log(response.data);
105
+ setIsLoading(false)
106
+ setSearchVideos(response.data.items)
107
+ } catch (error) {
108
+ alert("Rapid api not working using alternate api")
109
+
110
+ try {
111
+ const response2 = await axios.get(`https://www.googleapis.com/youtube/v3/search?part=snippet&q=business&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE1}&maxResults=50&type=video&videoDuration=medium`)
112
+ console.log(response2.data);
113
+ setIsLoading(false)
114
+ setSearchVideos(response2.data.items)
115
+ } catch (error) {
116
+ errorHandling(error)
117
+ }
118
+
119
+ }
120
+ }
121
+
122
+ return (
123
+ <YoutubeContext.Provider value={{ nextPageToken, setNextPageToken, isLoading, generateAutocomplete, autocomplete, trendingVideos, getTrendingVideos, setTrendingVideos, country, setCountry, getSearchVideos, searchVideos, setIsLoading, trendingVideosLength }}>
124
+ {children}
125
+ </YoutubeContext.Provider>
126
+ )
127
+ }
128
+
src/index.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
+ monospace;
13
+ }
src/index.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { ChakraProvider } from "@chakra-ui/react"
4
+ //import ReactGA from "react-ga4"
5
+
6
+ import "./index.css";
7
+ import App from "./App";
8
+ import { ContextProvider } from "./context/YoutubeContext";
9
+
10
+ //ReactGA.initialize(process.env.REACT_APP_GOOGLE_ANALYTICS_MEASUREMENT_ID)
11
+
12
+ const root = ReactDOM.createRoot(document.getElementById("root"));
13
+ root.render(
14
+ <React.StrictMode>
15
+ <ChakraProvider>
16
+ <ContextProvider>
17
+ <App />
18
+ </ContextProvider>
19
+ </ChakraProvider>
20
+ </React.StrictMode>
21
+ );
src/pages/home/Home.jsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from "react";
2
+ import { useLocation } from "react-router-dom";
3
+
4
+ import TrendingList from "../../components/list/TrendingList";
5
+ import SearchVideoList from "../../components/list/SearchList";
6
+ import Layout from "../../components/layout/Layout";
7
+
8
+ const Home = () => {
9
+ const location = useLocation();
10
+ const queryParams = new URLSearchParams(location.search);
11
+
12
+ useEffect(() => {
13
+ window.scrollTo(0, 0);
14
+ }, []);
15
+
16
+ // Access query parameters
17
+ const query = queryParams.get("query");
18
+
19
+ if (query) {
20
+ return (
21
+ <Layout>
22
+ <SearchVideoList />
23
+ </Layout>
24
+ );
25
+ } else {
26
+ return (
27
+ <Layout>
28
+ <TrendingList />
29
+ </Layout>
30
+ );
31
+ }
32
+ };
33
+
34
+ export default Home;
src/pages/video/Video.jsx ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable indent */
2
+ import React, { useEffect, useState } from "react";
3
+ import { useNavigate, useParams } from "react-router-dom";
4
+ import YouTube from "react-youtube";
5
+ //import ReactPlayer from "react-player";
6
+ import {
7
+ Avatar,
8
+ Box,
9
+ Button,
10
+ Flex,
11
+ IconButton,
12
+ Link,
13
+ Text,
14
+ } from "@chakra-ui/react";
15
+ import axios from "axios";
16
+ import { BiLike, BiDislike } from "react-icons/bi";
17
+ import { PiShareFatLight } from "react-icons/pi";
18
+ import { LiaDownloadSolid } from "react-icons/lia";
19
+ import { FiMoreHorizontal } from "react-icons/fi";
20
+ import numeral from "numeral";
21
+ import { formatDistanceToNow } from "date-fns";
22
+
23
+ import Header from "../../components/layout/Header";
24
+ import RelatedList from "../../components/list/RelatedList.jsx";
25
+ import { errorHandling } from "../../utils/utils";
26
+
27
+ const Video = () => {
28
+ const [videoDetails, setvideoDetails] = useState({});
29
+ const [channelDetails, setChannelDetails] = useState({});
30
+ const { videoId, channelId } = useParams();
31
+
32
+ const navigate = useNavigate();
33
+
34
+ const getVideoDetails = async () => {
35
+ try {
36
+ const res = await axios.get(
37
+ `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=${videoId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE2}`
38
+ );
39
+ setvideoDetails(res.data.items[0]);
40
+ console.log(res.data);
41
+ } catch (error) {
42
+ try {
43
+ const res = await axios.get(
44
+ `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=${videoId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE1}`
45
+ );
46
+ setvideoDetails(res.data.items[0]);
47
+ console.log(res.data);
48
+ } catch (error) {
49
+ errorHandling(error);
50
+ }
51
+ }
52
+ };
53
+
54
+ const getChannelDetails = async () => {
55
+ try {
56
+ const res2 = await axios.get(
57
+ `https://www.googleapis.com/youtube/v3/channels?part=snippet,contentDetails,statistics&id=${channelId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE2}`
58
+ );
59
+ console.log(res2.data);
60
+ setChannelDetails(res2.data.items[0]);
61
+ } catch (error) {
62
+ try {
63
+ const res2 = await axios.get(
64
+ `https://www.googleapis.com/youtube/v3/channels?part=snippet,contentDetails,statistics&id=${channelId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY_GOOGLE1}`
65
+ );
66
+ console.log(res2.data);
67
+ setChannelDetails(res2.data.items[0]);
68
+ } catch (error) {
69
+ errorHandling(error);
70
+ }
71
+ }
72
+ };
73
+
74
+ useEffect(() => {
75
+ const handleBackButton = () => {
76
+ navigate("/"); // Redirect to the home page
77
+ };
78
+
79
+ window.onpopstate = handleBackButton;
80
+
81
+ return () => {
82
+ window.onpopstate = null; // Clean up the event listener
83
+ };
84
+ }, []);
85
+
86
+ useEffect(() => {
87
+ getVideoDetails();
88
+ window.scrollTo(0, 0);
89
+ }, [videoId]);
90
+
91
+ useEffect(() => {
92
+ getChannelDetails();
93
+ }, [channelId]);
94
+
95
+ // Video Options
96
+ const opts1 = {
97
+ height: "650",
98
+ width: "100%",
99
+ showRelatedVideos: false,
100
+ fullscreen: true,
101
+ playsinline: true,
102
+ playerVars: {
103
+ autoplay: 0, // Auto-play the video
104
+ },
105
+ };
106
+
107
+ const opts2 = {
108
+ height: "210px",
109
+ width: "100%",
110
+ showRelatedVideos: false,
111
+ fullscreen: true,
112
+ playsinline: true,
113
+ playerVars: {
114
+ autoplay: 0, // Auto-play the video
115
+ },
116
+ };
117
+
118
+ return (
119
+ <>
120
+ <Box display={{ base: "block", sm: "none", md: "block" }}>
121
+ <Header />
122
+ </Box>
123
+ <Flex
124
+ gap={5}
125
+ minHeight={"90vh"}
126
+ width={"100%"}
127
+ padding={{ base: "5px 0", md: "3vh 5vw" }}
128
+ bg="#0f0f0f"
129
+ direction={{ base: "column", sm: "column", md: "row" }}
130
+ >
131
+ <Box width={{ md: "60vw" }}>
132
+ <Box
133
+ width={"100%"}
134
+ height={"70vh"}
135
+ borderRadius= {"1vw"}
136
+ overflow={"hidden"}
137
+ background={"#2e2c2c"}
138
+ display={{ base: "none", sm: "none", md: "block" }}
139
+ >
140
+ {/* <ReactPlayer url={`https://www.youtube.com/watch?v=${videoId}`}
141
+ className="react-player" controls/> */}
142
+ {/* <iframe
143
+ width={"100%"}
144
+ height={"100%"}
145
+ src={`https://www.youtube.com/embed/${videoId}`}
146
+ title="YouTube video player"
147
+ frameBorder="0"
148
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
149
+ allowFullScreen
150
+ ></iframe> */}
151
+ <YouTube videoId={videoId} opts={opts1} />
152
+ </Box>
153
+ <Box
154
+ position={"sticky"}
155
+ top={"0"}
156
+ display={{ base: "block", sm: "block", md: "none" }}
157
+ zIndex={13}
158
+ height={"210px"}
159
+ background={"#2e2c2c"}
160
+ >
161
+ {/* <ReactPlayer url={`https://www.youtube.com/watch?v=${videoId}`}
162
+ className="react-player" controls/> */}
163
+ {/* <iframe
164
+ width={"100%"}
165
+ height={"100%"}
166
+ src={`https://www.youtube.com/embed/${videoId}`}
167
+ title="YouTube video player"
168
+ frameBorder="0"
169
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
170
+ allowFullScreen
171
+ ></iframe> */}
172
+ <YouTube videoId={videoId} opts={opts2} />
173
+ </Box>
174
+
175
+ <Box>
176
+ <Text
177
+ color={"white"}
178
+ padding={"8px"}
179
+ fontSize={{ base: "lg", md: "22px" }}
180
+ >
181
+ {videoDetails?.snippet?.title}
182
+ </Text>
183
+
184
+ <VideoDetails
185
+ videoDetails={videoDetails}
186
+ channelDetails={channelDetails}
187
+ />
188
+ </Box>
189
+
190
+ <Box display={{ base: "block", sm: "block", md: "none" }}>
191
+ <RelatedList videoId={videoId} />
192
+ </Box>
193
+ </Box>
194
+
195
+ <Box display={{ base: "none", sm: "none", md: "block" }}>
196
+ <RelatedList videoId={videoId} />
197
+ </Box>
198
+ </Flex>
199
+ </>
200
+ );
201
+ };
202
+
203
+ export default Video;
204
+
205
+ const VideoDetails = ({ videoDetails, channelDetails }) => {
206
+ const [showMore, setShowMore] = useState(false);
207
+ const linkRegex = /(https?:\/\/[^\s]+)/g;
208
+
209
+ return (
210
+ <>
211
+ <Flex
212
+ direction={{ base: "column", sm: "column", md: "row" }}
213
+ align={{ base: "start", sm: "start", md: "center" }}
214
+ gap={{ md: 5 }}
215
+ justify={"space-between"}
216
+ >
217
+ <Flex
218
+ justify={{ base: "space-between" }}
219
+ padding={"8px"}
220
+ gap={3}
221
+ alignItems={"center"}
222
+ >
223
+ <Avatar
224
+ size={"md"}
225
+ name={channelDetails?.snippet?.title}
226
+ src={channelDetails?.snippet?.thumbnails?.medium?.url}
227
+ />
228
+ <Box>
229
+ <Text color={"white"}>{channelDetails?.snippet?.title}</Text>
230
+ <Text size={"xs"} color={"gray"}>
231
+ {viewsConverter(channelDetails?.statistics?.subscriberCount)}{" "}
232
+ subscribers
233
+ </Text>
234
+ </Box>
235
+ <Button
236
+ _hover={{ bg: "#b7b7b7" }}
237
+ bg="#e6e6e6"
238
+ color={"#303030"}
239
+ fontSize="14px"
240
+ borderRadius="20px"
241
+ size={"sm"}
242
+ alignSelf={"flex-end"}
243
+ >
244
+ Subscribe
245
+ </Button>
246
+ </Flex>
247
+ <Flex padding={"8px"} gap={1}>
248
+ <Box>
249
+ <Button
250
+ borderRadius={"20px 0 0 20px"}
251
+ color="white"
252
+ bg={"#303030"}
253
+ leftIcon={<BiLike size={"20px"} />}
254
+ borderRight={"1px"}
255
+ borderStyle={"solid"}
256
+ borderColor="white"
257
+ _hover={{ bg: "#424242" }}
258
+ fontSize={"14px"}
259
+ size={"sm"}
260
+ >
261
+ {viewsConverter(videoDetails?.statistics?.likeCount)}
262
+ </Button>
263
+ <IconButton
264
+ borderRadius={"0 20px 20px 0 "}
265
+ color="white"
266
+ _hover={{ bg: "#424242" }}
267
+ bg="#303030"
268
+ icon={<BiDislike size={"20px"} />}
269
+ size={"sm"}
270
+ />
271
+ </Box>
272
+
273
+ <Button
274
+ borderRadius={"20px"}
275
+ color="white"
276
+ _hover={{ bg: "#424242" }}
277
+ bg="#303030"
278
+ leftIcon={<PiShareFatLight size={"20px"} />}
279
+ fontSize={"14px"}
280
+ size={"sm"}
281
+ >
282
+ Share
283
+ </Button>
284
+
285
+ <Button
286
+ borderRadius={"20px"}
287
+ _hover={{ bg: "#424242" }}
288
+ bg="#303030"
289
+ color={"white"}
290
+ leftIcon={<LiaDownloadSolid size="20px" />}
291
+ fontSize={"14px"}
292
+ size={"sm"}
293
+ display={{ base: "none", md: "block" }}
294
+ >
295
+ Download
296
+ </Button>
297
+ <Button
298
+ borderRadius={"20px"}
299
+ _hover={{ bg: "#424242" }}
300
+ bg="#303030"
301
+ color={"white"}
302
+ leftIcon={<LiaDownloadSolid size="20px" />}
303
+ fontSize={"14px"}
304
+ size={"sm"}
305
+ display={{ base: "block", md: "none" }}
306
+ >
307
+ Stream on TG
308
+ </Button>
309
+
310
+ <IconButton
311
+ borderRadius={"20px"}
312
+ color="white"
313
+ _hover={{ bg: "#424242" }}
314
+ bg="#303030"
315
+ size={"sm"}
316
+ icon={<FiMoreHorizontal size={"20px"} />}
317
+ />
318
+ </Flex>
319
+ </Flex>
320
+
321
+ <Box
322
+ borderRadius={"20px"}
323
+ _hover={{ bg: "#424242", cursor: "pointer" }}
324
+ bg={"#303030"}
325
+ padding={"20px 15px"}
326
+ margin={{ base: "15px 8px", sm: "15px 8px", md: "15px 0" }}
327
+ >
328
+ <Text color={"white"}>
329
+ {viewsConverter(videoDetails?.statistics?.viewCount)} |{" "}
330
+ {videoDetails?.snippet?.publishedAt &&
331
+ timeConverter(videoDetails?.snippet?.publishedAt)}
332
+ </Text>
333
+ <Box
334
+ onClick={() => setShowMore(!showMore)}
335
+ color={"white"}
336
+ fontSize="15px"
337
+ whiteSpace={"pre-line"}
338
+ >
339
+ {showMore
340
+ ? videoDetails?.snippet?.description
341
+ .split(linkRegex)
342
+ .map((part, index) =>
343
+ linkRegex.test(part, index) ? (
344
+ <Link color={"#007bff"} href={part} key={index}>
345
+ {part}
346
+ </Link>
347
+ ) : (
348
+ part
349
+ )
350
+ )
351
+ : `${videoDetails?.snippet?.description
352
+ .split("")
353
+ .slice(0, 80)
354
+ .join("")}...`
355
+ .split(linkRegex)
356
+ .map((part, index) =>
357
+ linkRegex.test(part, index) ? (
358
+ <Link color={"#007bff"} href={part} key={index}>
359
+ {part}
360
+ </Link>
361
+ ) : (
362
+ part
363
+ )
364
+ )}
365
+ </Box>
366
+ </Box>
367
+ </>
368
+ );
369
+ };
370
+
371
+ // Views
372
+ const viewsConverter = (views) => {
373
+ const formattedViews = numeral(views).format("0.[00]a");
374
+
375
+ return formattedViews;
376
+ };
377
+
378
+ // Video Uploaded
379
+ const timeConverter = (time) => {
380
+ const date = new Date(time);
381
+ return formatDistanceToNow(date, { addSuffix: true });
382
+ };
src/utils/utils.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const errorHandling = (error) => {
2
+ console.log(error);
3
+
4
+ if (error.isAxiosError) {
5
+ if (error.code === "ERR_NETWORK")
6
+ return alert("Network error. Please check your internet connection.");
7
+
8
+ if (error.response) {
9
+ const { status } = error.response;
10
+
11
+ if (status === 403) {
12
+ return alert("API quota exceeded. Please try again later.");
13
+
14
+ } else {
15
+ return alert(`Request failed with status ${status}. Please try again.`);
16
+ }
17
+ }
18
+
19
+ if (error.code === "ECONNABORTED") {
20
+ return alert("Request timeout. Please check your network connection.");
21
+ } else {
22
+ return alert("An error occurred while fetching data. Please try again later.");
23
+ }
24
+
25
+ } else {
26
+ return alert("An unexpected error occurred. Please try again later.");
27
+ }
28
+ };