Spaces:
Paused
Paused
Bhabananda Das commited on
Commit ·
bd903ab
1
Parent(s): 3efcb14
fff
Browse files- .gitignore +24 -0
- Dockerfile +13 -0
- LICENSE +21 -0
- package-lock.json +0 -0
- package.json +60 -0
- public/favicon.ico +0 -0
- public/index.html +46 -0
- public/logo256.png +0 -0
- public/logo32.png +0 -0
- public/logo512.png +0 -0
- public/logo72.png +0 -0
- public/manifest.json +42 -0
- public/robots.txt +3 -0
- src/App.css +53 -0
- src/App.js +24 -0
- src/components/cards/HomeVideoCard.jsx +85 -0
- src/components/cards/RelatedVideoCard.jsx +65 -0
- src/components/cards/SearchVideoCard.jsx +93 -0
- src/components/constants/Constants.jsx +62 -0
- src/components/layout/Header.jsx +244 -0
- src/components/layout/HomeSkeleton.jsx +47 -0
- src/components/layout/Layout.jsx +38 -0
- src/components/layout/MobileSearch.jsx +107 -0
- src/components/layout/MobileSidebar.jsx +12 -0
- src/components/layout/SearchBox.jsx +168 -0
- src/components/layout/SearchSkeleton.jsx +70 -0
- src/components/layout/Sidebar.jsx +42 -0
- src/components/list/RelatedList.jsx +131 -0
- src/components/list/SearchList.jsx +103 -0
- src/components/list/TrendingList.jsx +252 -0
- src/context/YoutubeContext.js +128 -0
- src/index.css +13 -0
- src/index.js +21 -0
- src/pages/home/Home.jsx +34 -0
- src/pages/video/Video.jsx +382 -0
- src/utils/utils.js +28 -0
.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®ionCode=${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®ionCode=${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®ionCode=${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®ionCode=${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 |
+
};
|