├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── _redirects ├── index.html └── vite.svg ├── src ├── App.jsx ├── Components │ ├── CategoryList.jsx │ ├── ChannelVideoPage.jsx │ ├── ExploreVideos.jsx │ ├── Header.jsx │ ├── MainContainer.jsx │ ├── RelatedVideos.jsx │ ├── SearchBar.jsx │ ├── SearchVideoPage.jsx │ ├── ShimmerUI │ │ ├── ButtonsShimmer.jsx │ │ ├── SearchVideoShimmer.jsx │ │ └── VideoShimmer.jsx │ ├── SideBar.jsx │ ├── VideoCard.jsx │ └── VideoContainer.jsx ├── HomePageContainer │ ├── Body.jsx │ ├── CustomError.jsx │ ├── ExploreVideoPage.jsx │ ├── SearchResults.jsx │ ├── SubScriptionPage.jsx │ ├── WatchPage.jsx │ └── WatchPageData │ │ ├── ChannelData.jsx │ │ ├── ChatMessage.jsx │ │ ├── Comments.jsx │ │ ├── CommentsData.jsx │ │ ├── LiveChat.jsx │ │ └── RelatedVideoPage.jsx ├── assets │ ├── MyPic.jpg │ ├── YouTubeWhite.png │ ├── YouTube_Logo.png │ └── react.svg ├── index.css ├── main.jsx └── utils │ ├── APIList.jsx │ ├── appSlice.jsx │ ├── chatSlice.jsx │ ├── constants.jsx │ ├── helper.jsx │ ├── searchSlice.jsx │ └── store.jsx ├── tailwind.config.js └── vite.config.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # envFiles 2 | .env 3 | 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YOUTUBE CLONE : 2 | 3 | ### Project Overview 4 | 5 | Developed a highly optimized and scalable video streaming application, leveraging YouTube's Live API to deliver seamless live streaming experiences. The application focuses on enhancing user engagement through various optimization techniques and features. 6 | 7 | ### Live Demo : https://my-youtube-ui.netlify.app/ 8 | 9 | ### Key Features 10 | 11 | - Optimized Search (debouncing and caching techniques) 12 | - Live Chat Simulation (using API polling) 13 | - Lazy Loading 14 | - Shimmer UI while loading 15 | - Caching Search Suggestions 16 | - Hover to Seek Videos 17 | - Filter Videos by Tags 18 | - Experience on search results and explore pages (e.g., Trending). 19 | 20 | ### Technologies Used 21 | 22 | - React JS 23 | - Redux Toolkit 24 | - Tailwind CSS 25 | - Vite 26 | 27 | ### Getting Started : 28 | 29 | Follow these steps to set up and run the application locally: 30 | 31 | ### Prerequisites : 32 | 33 | - npm (Node Package Manager) installed. 34 | - Vite should be installed. 35 | 36 | ### Installation : 37 | 38 | 1. Clone the repository: 39 | 40 | ``` 41 | git clone https://github.com/onkar895/YouTube-UI.git 42 | cd MyYouTube 43 | ``` 44 | 45 | LOCALHOST=1234 46 | 47 | 2. Install dependencies: 48 | 49 | ``` 50 | cd MyYouTube 51 | npm install 52 | ``` 53 | 54 | 3. Run the code 55 | ``` 56 | npm run dev 57 | ``` 58 | 59 | Screenshots : 60 | ![Screenshot (69)](https://github.com/onkar895/YouTube-UI/assets/50394711/05fb5cd1-6278-4d2c-ab8a-0cdd0cf5fccc) 61 | ![Screenshot (54)](https://github.com/onkar895/YouTube-UI/assets/50394711/3bca374c-90d1-4d3b-8dfe-bf9c79874bcd) 62 | ![Screenshot (56)](https://github.com/onkar895/YouTube-UI/assets/50394711/50ffa619-d84d-4f1e-a57c-7051f40f112d) 63 | ![Screenshot (58)](https://github.com/onkar895/YouTube-UI/assets/50394711/7c7758f2-7392-46ec-9a9d-75ef725c06c8) 64 | ![Screenshot (59)](https://github.com/onkar895/YouTube-UI/assets/50394711/958376f7-75e6-41d9-abe5-b0a3ceb90438) 65 | ![Screenshot (61)](https://github.com/onkar895/YouTube-UI/assets/50394711/e7927f6a-acbc-41c6-8d10-08ee2a46cea1) 66 | ![Screenshot (62)](https://github.com/onkar895/YouTube-UI/assets/50394711/d2626bbc-8a14-48f8-9209-6cc2353a944b) 67 | ![Screenshot (63)](https://github.com/onkar895/YouTube-UI/assets/50394711/9fba1c39-e1df-4e44-8334-9c1bfbf1fc90) 68 | ![Screenshot (64)](https://github.com/onkar895/YouTube-UI/assets/50394711/6a25a783-9c54-4edb-8a02-5e4a31bc405e) 69 | ![Screenshot (65)](https://github.com/onkar895/YouTube-UI/assets/50394711/8310c62c-6c2d-451a-aebc-90c2569d33db) 70 | ![Screenshot (66)](https://github.com/onkar895/YouTube-UI/assets/50394711/4d4b18a6-91db-4a64-8b56-0a25dde39091) 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | YouTube 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myyoutube", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^2.0.1", 14 | "dotenv": "^16.3.1", 15 | "framer-motion": "^11.0.3", 16 | "random-avatar-generator": "^2.0.0", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-fontawesome": "^1.7.1", 20 | "react-icons": "^4.12.0", 21 | "react-infinite-scroll-component": "^6.1.0", 22 | "react-redux": "^9.0.2", 23 | "react-router": "^6.21.2", 24 | "react-router-dom": "^6.21.1", 25 | "redux": "^5.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.2.37", 29 | "@types/react-dom": "^18.2.15", 30 | "@vitejs/plugin-react": "^4.2.0", 31 | "autoprefixer": "^10.4.16", 32 | "eslint": "^8.53.0", 33 | "eslint-plugin-react": "^7.33.2", 34 | "eslint-plugin-react-hooks": "^4.6.0", 35 | "eslint-plugin-react-refresh": "^0.4.4", 36 | "postcss": "^8.4.31", 37 | "tailwind-scrollbar": "^3.0.5", 38 | "tailwindcss": "^3.3.5", 39 | "vite": "^5.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Not Found 7 | 45 | 46 | 47 |
48 |

404 - Page Not Found

49 |

The page you requested could not be found.

50 |

Visit the homepage

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | YouTube 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { lazy, Suspense } from 'react'; 3 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import store from './utils/store'; 6 | import "../src/index.css"; 7 | import Header from './Components/Header' 8 | import Body from './HomePageContainer/Body' 9 | import SearchResults from './HomePageContainer/SearchResults'; 10 | import MainContainer from './Components/MainContainer' 11 | import SubScriptionPage from './HomePageContainer/SubScriptionPage'; 12 | import ExploreVideoPage from './HomePageContainer/ExploreVideoPage'; 13 | 14 | // Lazy load WatchPage 15 | const WatchPage = lazy(() => import('./HomePageContainer/WatchPage')); 16 | 17 | const App = () => { 18 | return ( 19 | 20 |
21 | 22 |
23 | 24 | }> 25 | } /> 26 | Loading...}> 29 | 30 | 31 | } 32 | /> 33 | } /> 34 | } /> 35 | } /> 36 | 37 | 38 | 39 |
40 |
41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/Components/CategoryList.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { useState, useEffect, useRef } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useDispatch } from 'react-redux' 5 | import { fetchTagsUrl, YOUTUBE_SEARCH_API } from '../utils/APIList'; 6 | import { BiSolidChevronLeftCircle, BiSolidChevronRightCircle } from "react-icons/bi"; 7 | import ButtonsShimmer from './ShimmerUI/ButtonsShimmer'; 8 | import { TagNames } from '../utils/constants'; 9 | 10 | const CategoryList = () => { 11 | const listRef = useRef(); 12 | const navigate = useNavigate(); 13 | const [selectedButton, setSelectedButton] = useState("All"); 14 | const [loading, setLoading] = useState(true); 15 | const [slideNumber, setSlideNumber] = useState(0); 16 | const [tags, setTags] = useState([]); 17 | const [error, setError] = useState(null); 18 | const [isLoading, setIsLoading] = useState(false); 19 | 20 | 21 | const dispatch = useDispatch() 22 | 23 | useEffect(() => { 24 | const timer = setTimeout(() => { 25 | setLoading(false) 26 | }, 1000) 27 | 28 | return () => clearTimeout(timer); 29 | }, []); 30 | 31 | useEffect(() => { 32 | fetchTags(); 33 | }, []); 34 | 35 | const fetchTags = async () => { 36 | setLoading(true); 37 | try { 38 | const res = await fetch(fetchTagsUrl); 39 | const data = await res.json(); 40 | const items = data.items || []; // Extract items array from the response 41 | const tags = items.map(item => item.snippet.title); // Extract titles from items 42 | setTags(tags); 43 | setLoading(false); // Set loading to false after fetching tags 44 | } catch (error) { 45 | console.error('Error fetching tags:', error); 46 | setError('Failed to fetch tags. Please try again later.'); 47 | setLoading(false); 48 | } 49 | }; 50 | 51 | const handleExploreButtonClick = (ExploreName) => { 52 | const newQuery = ExploreName.replace(" ", "+"); 53 | setSelectedButton(newQuery); 54 | setIsLoading(true); // Start loading 55 | if (newQuery === "All") { 56 | navigate("/"); 57 | } else { 58 | navigate(`/explore?eq=${newQuery}`); 59 | } 60 | }; 61 | 62 | const handleScroll = (direction) => { 63 | const box = listRef.current; 64 | const slideWidth = box.clientWidth; 65 | const newSlideNumber = direction === "previous" ? slideNumber - 1 : slideNumber + 1; 66 | setSlideNumber(newSlideNumber); 67 | box.scrollLeft += (direction === "previous" ? -slideWidth : slideWidth); 68 | }; 69 | 70 | return ( 71 |
72 |
73 |
74 | 77 |
78 |
79 | 82 |
83 |
84 | 85 |
86 | 128 |
129 |
130 | ); 131 | }; 132 | 133 | export default CategoryList; 134 | 135 | -------------------------------------------------------------------------------- /src/Components/ChannelVideoPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-unused-vars */ 4 | import React, { useState, useEffect } from 'react'; 5 | import { CHANNEL_INFO_API, VIDEO_DETAILS_API } from '../utils/APIList'; 6 | import { CiBellOn } from 'react-icons/ci' 7 | import { HiOutlineChevronDown } from 'react-icons/hi2' 8 | import { formatTime, formatNumberWithSuffix, timeDuration } from '../utils/constants'; 9 | 10 | const ChannelVideoPage = ({ info, videoId }) => { 11 | 12 | const [videos, setVideos] = useState([]); 13 | const [channelPicture, setChannelPicture] = useState([]); 14 | const [channelInfo, setChannelInfo] = useState([]) 15 | const [subScribers, setSubScribers] = useState(0) 16 | const [subscribe, setSubscribe] = useState(false); 17 | const [isHovered, setIsHovered] = useState(false); 18 | 19 | const { snippet } = info; 20 | const { title, thumbnails, publishedAt, description, channelTitle } = snippet; 21 | 22 | const subscriberCount = formatNumberWithSuffix(subScribers); 23 | const viewCount = formatNumberWithSuffix(videos?.statistics?.viewCount); 24 | let calender = formatTime(publishedAt); 25 | const duration = timeDuration(videos?.contentDetails?.duration); 26 | 27 | const RemoveSpaces = channelTitle.replace(/ /g, "") 28 | 29 | useEffect(() => { 30 | fetchVideoData(); 31 | }, [videoId]); 32 | 33 | const fetchVideoData = async () => { 34 | try { 35 | const data = await fetch(VIDEO_DETAILS_API + '&id=' + videoId); 36 | const response = await data.json(); 37 | setVideos(response?.items?.[0] || {}); 38 | console.log(response?.items?.[0]) 39 | } catch (error) { 40 | console.log('Error while fetching video details', error); 41 | } 42 | }; 43 | 44 | useEffect(() => { 45 | if (info?.snippet?.channelId) { 46 | fetchChannelData(); 47 | } 48 | }, [info?.snippet?.channelId]); 49 | 50 | const fetchChannelData = async () => { 51 | try { 52 | const data = await fetch(CHANNEL_INFO_API + '&id=' + info?.snippet?.channelId); 53 | const response = await data.json(); 54 | setChannelInfo(response?.items[0]) 55 | const profilePictureUrl = response?.items?.[0]?.snippet?.thumbnails?.default?.url || ''; 56 | setChannelPicture(profilePictureUrl); 57 | const subScribers = response?.items?.[0]?.statistics?.subscriberCount || '' 58 | setSubScribers(subScribers) 59 | } catch (error) { 60 | console.log("Couldn't fetch channel profile picture", error); 61 | } 62 | }; 63 | 64 | return ( 65 | <> 66 |
setIsHovered(true)} 68 | onMouseOut={() => setIsHovered(false)}> 69 |
70 | { 71 | isHovered && ( 72 |
73 | 83 |
84 | ) 85 | } 86 | thumbnail 91 |
92 | {duration} 93 |
94 |
95 |
96 |
97 |

{title}

98 |
99 | ChannelProfile 100 | {channelTitle} 101 |
102 |
103 | {viewCount} Views 104 | 105 | {calender} 106 |
107 |
108 |
109 |
110 | 111 | ); 112 | }; 113 | 114 | export default ChannelVideoPage; -------------------------------------------------------------------------------- /src/Components/ExploreVideos.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-unused-vars */ 4 | import React, { useState, useEffect } from 'react'; 5 | import { CHANNEL_INFO_API, VIDEO_DETAILS_API } from '../utils/APIList'; 6 | import { formatTime, formatNumberWithSuffix, timeDuration } from '../utils/constants'; 7 | 8 | const ExploreVideos = ({ info, videoId }) => { 9 | 10 | const [videos, setVideos] = useState([]); 11 | const [channelPicture, setChannelPicture] = useState(''); 12 | const [channelInfo, setChannelInfo] = useState([]) 13 | const [isHovered, setIsHovered] = useState(false); 14 | const { snippet } = info; 15 | const { title, thumbnails, publishedAt, description, channelTitle } = snippet; 16 | 17 | const viewCount = formatNumberWithSuffix(videos?.statistics?.viewCount); 18 | let calender = formatTime(publishedAt); 19 | const duration = timeDuration(videos?.contentDetails?.duration); 20 | 21 | useEffect(() => { 22 | fetchVideoData(); 23 | }, [videoId]); 24 | 25 | const fetchVideoData = async () => { 26 | try { 27 | const data = await fetch(VIDEO_DETAILS_API + '&id=' + videoId); 28 | const response = await data.json(); 29 | setVideos(response?.items?.[0] || {}); 30 | // console.log(response?.items?.[0]) 31 | } catch (error) { 32 | console.log('Error while fetching video details', error); 33 | } 34 | }; 35 | 36 | useEffect(() => { 37 | if (info?.snippet?.channelId) { 38 | fetchChannelData(); 39 | } 40 | }, [info?.snippet?.channelId]); 41 | 42 | const fetchChannelData = async () => { 43 | try { 44 | const data = await fetch(CHANNEL_INFO_API + '&id=' + info?.snippet?.channelId); 45 | const response = await data.json(); 46 | setChannelInfo(response?.items[0]) 47 | const profilePictureUrl = response?.items?.[0]?.snippet?.thumbnails?.default?.url || ''; 48 | setChannelPicture(profilePictureUrl); 49 | } catch (error) { 50 | console.log("Couldn't fetch channel profile picture", error); 51 | } 52 | }; 53 | 54 | return ( 55 |
setIsHovered(true)} 56 | onMouseOut={() => setIsHovered(false)}> 57 |
58 | { 59 | isHovered && ( 60 |
61 | 71 |
72 | ) 73 | } 74 | 75 | thumbnail 80 |
81 | {duration} 82 |
83 |
84 |
85 |
86 |

{title}

87 |
88 | ChannelProfile 89 | {channelTitle} 90 |
91 |
92 | {viewCount} Views 93 | 94 | {calender} 95 |
96 |
97 |
98 |
99 | ); 100 | }; 101 | 102 | export default ExploreVideos; -------------------------------------------------------------------------------- /src/Components/Header.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unescaped-entities */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState } from 'react'; 4 | import { CgMenuLeftAlt } from "react-icons/cg"; 5 | import { IoNotificationsOutline } from 'react-icons/io5'; 6 | import { RiVideoAddLine } from 'react-icons/ri'; 7 | import YouTubeLogo from '../assets/YouTube_Logo.png'; 8 | import MyPic from '../assets/MyPic.jpg'; 9 | import { useDispatch } from 'react-redux'; 10 | import { toggleMenu } from '../utils/appSlice'; 11 | import { AiFillAudio } from "react-icons/ai"; 12 | import { useNavigate } from 'react-router-dom'; 13 | import SearchBar from './SearchBar'; 14 | 15 | const Header = () => { 16 | const dispatch = useDispatch(); 17 | 18 | const [showSearch, setShowSearch] = useState(false); 19 | 20 | const navigate = useNavigate() 21 | 22 | function handleClick() { 23 | navigate("/"); 24 | } 25 | 26 | // Function to toggle the side menu 27 | const toggleMenuHandler = () => { 28 | dispatch(toggleMenu()); 29 | }; 30 | 31 | return ( 32 | 67 | ); 68 | }; 69 | 70 | export default Header; -------------------------------------------------------------------------------- /src/Components/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import CategoryList from './CategoryList' 4 | import VideoContainer from '../Components/VideoContainer' 5 | 6 | const MainContainer = () => { 7 | 8 | return ( 9 | <> 10 |
11 | 12 | 13 |
14 | 15 | ) 16 | } 17 | 18 | export default MainContainer -------------------------------------------------------------------------------- /src/Components/RelatedVideos.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState } from 'react' 4 | import { timeDuration, formatTime, formatNumberWithSuffix } from '../utils/constants' 5 | import { truncateText } from '../utils/helper' 6 | 7 | const RelatedVideos = ({ info, videoId }) => { 8 | const [isHovered, setIsHovered] = useState(false); 9 | 10 | const { snippet, contentDetails, statistics } = info; 11 | const { title, thumbnails, channelTitle, publishedAt } = snippet; 12 | 13 | const duration = timeDuration(contentDetails.duration); 14 | const calender = formatTime(publishedAt); 15 | const viewCount = statistics?.viewCount ? formatNumberWithSuffix(statistics.viewCount) : 0; 16 | 17 | return ( 18 |
setIsHovered(true)} 19 | onMouseOut={() => setIsHovered(false)}> 20 |
21 | { 22 | isHovered && ( 23 |
24 | 34 |
35 | ) 36 | } 37 | 38 |
39 | thumbnail 44 |
45 | {duration} 46 |
47 |
48 |
49 |
50 |
51 |

{truncateText(title, 60)}

52 |
53 | {viewCount} Views 54 | {calender} 55 |
56 |
57 |
58 | {channelTitle} 59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | export default RelatedVideos 66 | 67 | -------------------------------------------------------------------------------- /src/Components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-unused-vars */ 4 | import React, { useEffect, useRef, useState } from 'react'; 5 | import { IoSearchOutline } from 'react-icons/io5'; 6 | import { BsArrowLeftShort } from 'react-icons/bs'; 7 | import { IoCloseOutline } from "react-icons/io5"; 8 | import { YOUTUBE_SEARCH_SUGGESTION_API } from '../utils/APIList'; 9 | import { useSelector } from 'react-redux'; 10 | import { cacheResults } from '../utils/searchSlice'; 11 | import { useDispatch } from 'react-redux'; 12 | import { useNavigate } from 'react-router-dom'; 13 | 14 | const SearchBar = ({ showSearch, setShowSearch }) => { 15 | const isMenuOpen = useSelector((store) => store.app.isMenuOpen); 16 | const [searchQuery, setSearchQuery] = useState(''); 17 | const [suggestions, setSuggestions] = useState([]); 18 | const [showSuggestions, setShowSuggestions] = useState(false); 19 | const [isInputFocused, setIsInputFocused] = useState(false); 20 | 21 | const searchCache = useSelector((store) => store.search) 22 | const search = useRef(null); 23 | const dispatch = useDispatch() 24 | const navigate = useNavigate() 25 | 26 | // Styles stored in variables for better readability and maintainability 27 | const searchSuggestionBarStyles = `${showSearch ? 'max-sm:w-[98%] max-sm:rounded-b-2xl max-sm:h-full max-sm:border-none' : 'max-sm:hidden'} fixed py-5 bg-white md:shadow-2xl md:rounded-2xl md:w-[45vw] lg:w-[42.3vw] lg:h-[75vh] border border-gray-100 `; 28 | 29 | const inputStyles = `${showSearch ? 'w-[61vw] max-sm:mx-auto transition-all duration-500 max-sm:ml-2 ml-2 pl-[12px] py-2' : 'max-sm:hidden'} ${isInputFocused ? 'max-sm:w-[74vw] max-sm:mx-auto max-sm:focus:outline-0 max-sm:border max-sm:border-red-600 md:pl-[3.2rem] md:border md:border-blue-600' : ''} md:w-[36vw] lg:w-[42vw] md:py-[7px] lg:py-[7px] border border-gray-300 rounded-l-full py-1 pl-3 md:pl-6 items-center transition-all focus:outline-0 duration-500}`; 30 | 31 | const searchButtonStyles = `${showSearch ? 'max-sm:px-3 max-sm:py-[10px]' : 'max-sm:border-none max-sm:text-2xl max-sm:px-auto max-sm:ml-32'} text-xl px-[2px] py-[9px] border border-gray-300 md:hover:bg-gray-200 rounded-r-full md:px-6 flex justify-center items-center md:bg-gray-100`; 32 | 33 | /** 34 | * Suppose, 35 | * searchCache = { 36 | * "iphone" : ["iphone 13", "iphone 14", .....] 37 | * } 38 | * 39 | * searchQuery = iphone 40 | */ 41 | 42 | useEffect(() => { 43 | const timer = setTimeout(() => { 44 | // If there are cached suggestions for the current searchQuery, it sets those suggestions using setSuggestions. 45 | // If there are no cached suggestions, it calls the getSearchSuggestions function. 46 | if (searchCache[searchQuery]) { 47 | setSuggestions((searchCache[searchQuery])) 48 | } else { 49 | getSearchSuggestions() 50 | } 51 | }, 200); 52 | 53 | return () => { 54 | clearTimeout(timer); 55 | } 56 | }, [searchQuery]); 57 | 58 | const getSearchSuggestions = async () => { 59 | try { 60 | console.log('API CALL - ' + searchQuery); 61 | const data = await fetch(YOUTUBE_SEARCH_SUGGESTION_API + searchQuery); 62 | const response = await data.json(); 63 | setSuggestions(response[1]); 64 | 65 | // Dispatching a Redux action (cacheResults) to update the search cache with the new suggestions. 66 | dispatch(cacheResults({ 67 | [searchQuery]: response[1] // key:value 68 | })) 69 | 70 | } catch (error) { 71 | console.error('Error while fetching search suggestions:', error); 72 | } 73 | }; 74 | 75 | // Function to handle the search button click 76 | const handleSearchButtonClick = () => { 77 | const screenWidth = window.innerWidth; 78 | if (screenWidth < 768 && !showSearch) { 79 | setShowSearch(true); 80 | } 81 | }; 82 | 83 | // Function to handle the arrow left button click in the search 84 | const handleArrowLeftClick = () => { 85 | setShowSearch(false); 86 | }; 87 | 88 | // Function to handle input focus 89 | const handleInputFocus = () => { 90 | setIsInputFocused(true); 91 | setShowSuggestions(true); 92 | }; 93 | 94 | // Function to handle input blur 95 | const handleInputBlur = () => { 96 | setIsInputFocused(false); 97 | // setShowSuggestions(false); 98 | }; 99 | 100 | const handleClearSearch = (event) => { 101 | event.preventDefault(); 102 | setSearchQuery(''); 103 | search.current.focus() 104 | }; 105 | 106 | const handleScrollTop = () => { 107 | window.scrollTo({ 108 | top: 0, 109 | behavior: "smooth", 110 | }); 111 | }; 112 | 113 | const handleSearch = (event, search) => { 114 | event.preventDefault(); 115 | if (search !== "") { 116 | const query = search.replace(" ", "+"); 117 | navigate(`/results?search_query=${query}`); 118 | handleScrollTop(); 119 | setShowSuggestions(false); 120 | setSearchQuery(""); 121 | } 122 | }; 123 | 124 | return ( 125 | <> 126 |
127 |
128 | {/* Left Arrow Button In sm Search To Move to normal Screen */} 129 | { 130 | showSearch && ( 131 |
132 | 135 |
136 | ) 137 | } 138 | 139 | { 140 | isInputFocused && ( 141 |
142 | 145 |
146 | ) 147 | } 148 | 149 | { 150 | searchQuery && ( 151 |
152 | 155 |
156 | ) 157 | } 158 | 159 | setSearchQuery(e.target.value)} 166 | onFocus={handleInputFocus} 167 | onBlur={handleInputBlur} 168 | /> 169 | 172 |
173 | 174 |
175 | { 176 | searchQuery && showSuggestions && ( 177 |
178 | { 179 | suggestions.length === 0 ? ( 180 | searchQuery !== "" && ( 181 |

{`No Results Found for ${searchQuery}`}

182 | ) 183 | ) : ( 184 |
    185 | { 186 | suggestions.map((suggestion) => ( 187 |
  • handleSearch(event, suggestion)} > 188 | 189 | {suggestion} 190 |
  • 191 | )) 192 | } 193 |
194 | ) 195 | } 196 |
197 | ) 198 | } 199 |
200 |
201 | 202 | ); 203 | }; 204 | 205 | export default SearchBar; 206 | -------------------------------------------------------------------------------- /src/Components/SearchVideoPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-unused-vars */ 4 | import React, { useState, useEffect } from 'react'; 5 | import { CHANNEL_INFO_API, VIDEO_DETAILS_API } from '../utils/APIList'; 6 | import { formatTime, formatNumberWithSuffix, timeDuration } from '../utils/constants'; 7 | import { truncateText } from '../utils/helper' 8 | 9 | 10 | const SearchVideoPage = ({ info, videoId }) => { 11 | 12 | const [videos, setVideos] = useState([]); 13 | const [channelPicture, setChannelPicture] = useState([]); 14 | const [channelInfo, setChannelInfo] = useState([]) 15 | const [isHovered, setIsHovered] = useState(false); 16 | const { snippet } = info; 17 | const { title, thumbnails, publishedAt, description, channelTitle } = snippet; 18 | 19 | const viewCount = formatNumberWithSuffix(videos?.statistics?.viewCount); 20 | let calender = formatTime(publishedAt); 21 | const duration = timeDuration(videos?.contentDetails?.duration); 22 | 23 | useEffect(() => { 24 | fetchVideoData(); 25 | }, [videoId]); 26 | 27 | const fetchVideoData = async () => { 28 | try { 29 | const data = await fetch(VIDEO_DETAILS_API + '&id=' + videoId); 30 | const response = await data.json(); 31 | setVideos(response?.items?.[0] || {}); // Ensure items array exists 32 | console.log(response?.items?.[0]) 33 | } catch (error) { 34 | console.log('Error while fetching video details', error); 35 | } 36 | }; 37 | 38 | useEffect(() => { 39 | if (info?.snippet?.channelId) { 40 | fetchChannelData(); 41 | } 42 | }, [info?.snippet?.channelId]); 43 | 44 | const fetchChannelData = async () => { 45 | try { 46 | const data = await fetch(CHANNEL_INFO_API + '&id=' + info?.snippet?.channelId); 47 | const response = await data.json(); 48 | setChannelInfo(response?.items[0]) 49 | const profilePictureUrl = response?.items?.[0]?.snippet?.thumbnails?.default?.url || ''; 50 | setChannelPicture(profilePictureUrl); 51 | } catch (error) { 52 | console.log("Couldn't fetch channel profile picture", error); 53 | } 54 | }; 55 | 56 | return ( 57 |
setIsHovered(true)} onMouseOut={() => setIsHovered(false)}> 58 |
59 | { 60 | isHovered && ( 61 |
62 | 72 |
73 | ) 74 | } 75 | 76 |
77 | thumbnail 82 |
83 | {duration} 84 |
85 |
86 |
87 |
88 |
89 |

{truncateText(title, 60)}

90 |
91 | {viewCount} Views 92 | 93 | {calender} 94 |
95 |
96 |
97 | ChannelProfile 98 | {channelTitle} 99 |
100 |
101 | {truncateText(description, 90)} 102 |
103 |
104 |
105 | ); 106 | }; 107 | 108 | export default SearchVideoPage; -------------------------------------------------------------------------------- /src/Components/ShimmerUI/ButtonsShimmer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react'; 3 | 4 | const ButtonsShimmer = () => { 5 | return ( 6 |
7 | {Array(12).fill('').map((_, index) => ( 8 |
9 |
10 |
11 | ))} 12 |
13 | ); 14 | }; 15 | 16 | export default ButtonsShimmer; 17 | -------------------------------------------------------------------------------- /src/Components/ShimmerUI/SearchVideoShimmer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | 4 | const SearchVideoShimmer = () => { 5 | return ( 6 |
7 | { 8 | // creating a new Array instance using Array() constructor and map through every element of array 9 | Array(50) 10 | .fill("") 11 | .map((_, index) => ( 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )) 28 | } 29 |
30 | ) 31 | } 32 | 33 | export default SearchVideoShimmer 34 | -------------------------------------------------------------------------------- /src/Components/ShimmerUI/VideoShimmer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | 4 | const VideoShimmer = () => { 5 | return ( 6 |
7 | { 8 | // creating the new Array instance using Array() constructor and map through every element of array 9 | Array(50) 10 | .fill("") 11 | .map((_, index) => ( 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {/*
25 | 26 |
27 |
28 |
*/} 29 |
30 | 31 | )) 32 | } 33 |
34 | ) 35 | } 36 | 37 | export default VideoShimmer 38 | -------------------------------------------------------------------------------- /src/Components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useEffect, useState } from 'react'; 4 | import { Explore, Premium, Setting, Home, Subscriptions } from '../utils/constants'; 5 | import { MdHomeFilled } from 'react-icons/md'; 6 | import { FaSquareYoutube } from 'react-icons/fa6'; 7 | import { SiYoutubeshorts } from 'react-icons/si'; 8 | import { MdSubscriptions } from 'react-icons/md'; 9 | import { CgMenuLeftAlt } from "react-icons/cg"; 10 | import YouTubeLogo from '../assets/YouTube_Logo.png'; 11 | import { useDispatch } from 'react-redux'; 12 | import { toggleMenu } from '../utils/appSlice'; 13 | import { useNavigate } from 'react-router-dom'; 14 | import { useSelector } from 'react-redux'; 15 | import { NavLink, useLocation, useSearchParams } from 'react-router-dom'; 16 | 17 | const SideBar = () => { 18 | 19 | const [searchParams] = useSearchParams(); 20 | const [selectedButton, setSelectedButton] = useState("Home"); 21 | const [isLoading, setIsLoading] = useState(false); 22 | 23 | const isMenuOpen = useSelector((store) => store.app.isMenuOpen) 24 | 25 | const location = useLocation(); 26 | 27 | const dispatch = useDispatch(); 28 | 29 | const navigate = useNavigate() 30 | 31 | // useEffect(() => { 32 | // const Query = searchParams.get("eId"); 33 | // if (Query) { 34 | // setSelectedButton(Query); 35 | // } else { 36 | // setSelectedButton("Home"); 37 | // } 38 | // }, []) 39 | 40 | function handleClick() { 41 | navigate("/"); 42 | } 43 | 44 | const StopLoading = () => { 45 | setTimeout(() => { 46 | // Close sidebar after 1000ms 47 | dispatch(toggleMenu()); 48 | setIsLoading(false); // Stop loading 49 | }, 500); 50 | } 51 | 52 | const handleChannelButtonClick = (ButtonName) => { 53 | const Query = ButtonName.replace(" ", "+"); 54 | setSelectedButton(Query); 55 | setIsLoading(true); // Start loading 56 | StopLoading() // Stop Loading and Close sidebar after 500ms 57 | if (Query === "Home") { 58 | navigate("/"); 59 | } else { 60 | navigate(`/channel?cId=${Query}`); 61 | } 62 | }; 63 | 64 | const handleExploreButtonClick = (ExploreName) => { 65 | const newQuery = ExploreName.replace(" ", "+"); 66 | setSelectedButton(newQuery); 67 | setIsLoading(true); // Start loading 68 | StopLoading() // Stop Loading and Close sidebar after 500ms 69 | if (newQuery === "Home") { 70 | navigate("/"); 71 | } else { 72 | navigate(`/explore?eq=${newQuery}`); 73 | } 74 | }; 75 | 76 | // Function to toggle the side menu 77 | const toggleMenuHandler = () => { 78 | dispatch(toggleMenu()); 79 | }; 80 | 81 | const SideBarStyle = isMenuOpen 82 | ? ' sidebar-open fixed left-0 md:w-[30vw] lg:w-[19vw] max-sm:w-[60vw] h-full bg-white z-50 text-sm md:top-0 max-sm:top-0 shadow-gray-700 shadow-2xl transition-shadow duration-300' 83 | : 'fixed max-sm:hidden md:flex-col text-xs space-y-6 mt-[73px] ml-1' 84 | 85 | // Early return pattern 86 | // If the menu is closed and the location is '/watch', hide the first sidebar 87 | if (!isMenuOpen) { 88 | if (location.pathname === '/watch') { 89 | return null; 90 | } 91 | 92 | // Render the first sidebar with four sections when the menu is closed. 93 | return ( 94 |
95 |
96 | 97 | 98 | Home 99 | 100 |
101 |
102 | 103 | Shorts 104 |
105 |
106 | 107 | Subscriptions 108 |
109 |
110 | 111 | You 112 |
113 |
114 | ); 115 | } 116 | 117 | // If the menu is open, render the toggle sidebar with all sections. 118 | return ( 119 | <> 120 |
121 |
122 |
123 | 124 |
125 | YouTubeLogo 126 |
127 |
128 | 144 | 145 |
146 |
147 |
148 | 149 | 167 | 168 |
169 |
170 |
171 | 172 | 189 | 190 |
191 |
192 |
193 | 194 | 211 | 212 |
213 |
214 |
215 | 216 | 232 |
233 |
234 | 235 | ); 236 | }; 237 | 238 | export default SideBar; -------------------------------------------------------------------------------- /src/Components/VideoCard.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-unused-vars */ 4 | import React, { useState, useEffect } from 'react'; 5 | import { LuEye } from 'react-icons/lu'; 6 | import { BiLike } from "react-icons/bi"; 7 | import { timeDuration, formatTime, formatNumberWithSuffix } from '../utils/constants'; 8 | import { CHANNEL_INFO_API } from '../utils/APIList'; 9 | import VideoShimmer from './ShimmerUI/VideoShimmer'; 10 | 11 | const VideoCard = ({ info }) => { 12 | const [isHovered, setIsHovered] = useState(false); 13 | const [profilePicture, setProfilePicture] = useState([]); 14 | 15 | useEffect(() => { 16 | if (info) { 17 | fetchProfilePicture(); 18 | } 19 | }, [info]); 20 | 21 | const fetchProfilePicture = async () => { 22 | try { 23 | const data = await fetch(CHANNEL_INFO_API + "&id=" + info.snippet.channelId); 24 | const response = await data.json(); 25 | const profilePictureUrl = response?.items[0]?.snippet?.thumbnails?.default?.url; 26 | setProfilePicture(profilePictureUrl); 27 | } catch (error) { 28 | console.error("Couldn't fetch channel profile picture", error); 29 | } 30 | }; 31 | 32 | const { snippet, statistics, contentDetails } = info; 33 | const { title, channelTitle, thumbnails, publishedAt } = snippet; 34 | const duration = timeDuration(contentDetails.duration); 35 | const calender = formatTime(publishedAt); 36 | const viewCount = statistics?.viewCount ? formatNumberWithSuffix(statistics.viewCount) : 0; 37 | const likeCount = statistics?.likeCount ? formatNumberWithSuffix(statistics.likeCount) : 0; 38 | 39 | return ( 40 |
setIsHovered(true)} 42 | onMouseOut={() => setIsHovered(false)}> 43 |
44 | { 45 | isHovered && ( 46 |
47 | 57 |
58 | ) 59 | } 60 | thumbnail 65 |
66 | {duration} 67 |
68 |
69 | 95 |
96 | ); 97 | }; 98 | 99 | export default VideoCard; 100 | -------------------------------------------------------------------------------- /src/Components/VideoContainer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { useEffect, useState } from 'react'; 3 | import { YOUTUBE_VIDEO_API, YOUTUBE_SEARCHCATEGORY_API } from '../utils/APIList'; 4 | import VideoCard from '../Components/VideoCard'; 5 | import VideoShimmer from './ShimmerUI/VideoShimmer'; 6 | import CustomError from '../HomePageContainer/CustomError'; 7 | import { NavLink } from 'react-router-dom'; 8 | 9 | const VideoContainer = () => { 10 | const [videos, setVideos] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | const [error, setError] = useState(null); 13 | 14 | useEffect(() => { 15 | // Simulate loading data 16 | const timer = setTimeout(() => { 17 | setLoading(false); 18 | }, 2000); 19 | 20 | return () => clearTimeout(timer); 21 | }, []); 22 | 23 | useEffect(() => { 24 | getVideos() 25 | }, []); 26 | 27 | const getVideos = async () => { 28 | try { 29 | const response = await fetch(YOUTUBE_VIDEO_API); 30 | 31 | if (!response.ok) { 32 | throw new Error(`Failed to fetch videos. Status: ${response.status}`); 33 | } 34 | 35 | const data = await response.json(); 36 | setVideos(data.items); 37 | setError(null); // Clear error state on successful fetch 38 | } catch (error) { 39 | console.error('Error while fetching the videos:', error); 40 | setError('Failed to fetch videos. Please try again later.'); 41 | } 42 | }; 43 | 44 | if (error) { 45 | return ; 46 | } 47 | 48 | return ( 49 |
50 | {loading ? ( 51 | 52 | ) : ( 53 | videos.map((video) => ( 54 | 55 | 56 | 57 | )) 58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export default VideoContainer; 64 | -------------------------------------------------------------------------------- /src/HomePageContainer/Body.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import SideBar from '../Components/SideBar' 4 | import { Outlet } from 'react-router-dom' 5 | import { useSelector } from 'react-redux'; 6 | 7 | const Body = () => { 8 | 9 | const isMenuOpen = useSelector((store) => store.app.isMenuOpen) 10 | 11 | const mainStyle = isMenuOpen ? 'blur-effect' : '' 12 | return ( 13 |
14 | 15 | {/* { 16 | The < Outlet > component from react-router-dom is a special component that is used within a parent route to render the child routes. In this case, it allows the child routes defined in the App component to be rendered inside the Body component. 17 | } */} 18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default Body 26 | -------------------------------------------------------------------------------- /src/HomePageContainer/CustomError.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable react/no-unescaped-entities */ 4 | import React from "react"; 5 | 6 | const Error = ({ message }) => { 7 | 8 | const classNameString = `bg-black font-bold tracking-wide rounded-2xl shadow-md text-white text-justify box-border lg:w-[48%] md:mt-12 max-sm:mt-12 max-sm:w-[94vw] lg:py-6 lg:px-6 max-sm:py-6 max-sm:px-5 max-sm:mx-3 lg:ml-[24.5%] lg:min-h-[20vh] md:w-[68%] md:py-6 md:px-6 md:ml-[15%] relative` 9 | 10 | return ( 11 | <> 12 |
13 |
14 |

{message}

15 |
16 |

- This service, powered by free APIs with limitations, may experience interruptions.

17 |

- The API key has reached the maximum number of allowed requests. So, it breaks anytime! Sorry for the inconvenience.

18 |

- For a more reliable experience, kindly consider using the original YouTube platform.

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Error; -------------------------------------------------------------------------------- /src/HomePageContainer/ExploreVideoPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState, useEffect } from 'react'; 4 | import { YOUTUBE_SEARCH_API } from '../utils/APIList'; 5 | import { NavLink, useSearchParams } from 'react-router-dom'; 6 | import CustomError from './CustomError' 7 | import ExploreVideos from '../Components/ExploreVideos' 8 | import VideoShimmer from '../Components/ShimmerUI/VideoShimmer' 9 | import CategoryList from '../Components/CategoryList'; 10 | 11 | const ExploreVideoPage = () => { 12 | const [videos, setVideos] = useState([]); 13 | const [isLoading, setIsLoading] = useState(true); 14 | const [error, setError] = useState(null); 15 | 16 | const [searchParam] = useSearchParams(); 17 | const explore = searchParam.get('eq'); 18 | 19 | useEffect(() => { 20 | const timer = setTimeout(() => { 21 | setIsLoading(false); 22 | }, 2000); 23 | 24 | return () => clearTimeout(timer); 25 | }, []); 26 | 27 | useEffect(() => { 28 | getExploreResults(); 29 | }, [explore]); 30 | 31 | const getExploreResults = async () => { 32 | try { 33 | if (!explore) return; 34 | const response = await fetch(`${YOUTUBE_SEARCH_API}&q=${explore}®ionCode=IN&type=video`); 35 | if (!response.ok) { 36 | throw new Error(`Failed to fetch search results. Status: ${response.status}`); 37 | } 38 | const data = await response.json(); 39 | setVideos(data.items || []); 40 | setError(null); 41 | } catch (error) { 42 | console.error('Error while fetching search videos', error); 43 | setError('Failed to fetch videos. Please try again later.'); 44 | } 45 | }; 46 | 47 | return ( 48 | <> 49 |
50 |
51 | 52 |
53 |
54 | { 55 | isLoading ? ( 56 | 57 | ) : error ? ( 58 |
59 | 60 |
61 | ) : ( 62 | videos.map((video) => ( 63 | 64 | 65 | 66 | )) 67 | ) 68 | } 69 |
70 |
71 | 72 | ); 73 | }; 74 | 75 | export default ExploreVideoPage; -------------------------------------------------------------------------------- /src/HomePageContainer/SearchResults.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState, useEffect } from 'react'; 4 | import { YOUTUBE_SEARCH_API } from '../utils/APIList'; 5 | import { NavLink, useSearchParams } from 'react-router-dom'; 6 | import CustomError from './CustomError' 7 | import SearchVideoPage from '../Components/SearchVideoPage'; 8 | import SearchVideoShimmer from '../Components/ShimmerUI/SearchVideoShimmer'; 9 | import CategoryList from '../Components/CategoryList'; 10 | 11 | const SearchResults = () => { 12 | const [searchParam] = useSearchParams(); 13 | const searchQuery = searchParam.get('search_query'); 14 | const [videos, setVideos] = useState([]); 15 | const [isLoading, setIsLoading] = useState(true); 16 | const [error, setError] = useState(null); 17 | 18 | useEffect(() => { 19 | const timer = setTimeout(() => { 20 | setIsLoading(false); 21 | }, 2000); 22 | 23 | return () => clearTimeout(timer); 24 | }, []); 25 | 26 | useEffect(() => { 27 | getSearchResults(); 28 | }, [searchQuery]); 29 | 30 | const getSearchResults = async () => { 31 | try { 32 | if (!searchQuery) return; 33 | const response = await fetch(`${YOUTUBE_SEARCH_API}&q=${searchQuery}®ionCode=IN&type=video`); 34 | if (!response.ok) { 35 | throw new Error(`Failed to fetch search results. Status: ${response.status}`); 36 | } 37 | const data = await response.json(); 38 | setVideos(data.items || []); 39 | setError(null); 40 | } catch (error) { 41 | console.error('Error while fetching search videos', error); 42 | setError('Failed to fetch videos. Please try again later.'); 43 | } 44 | }; 45 | 46 | return ( 47 | <> 48 |
49 |
50 | 51 |
52 |
53 | { 54 | !error ? ( 55 |

Showing Results for : {searchQuery}

56 | ) : ( 57 |

Unable to show any Results for : {searchQuery}

58 | ) 59 | } 60 |
61 |
62 | { 63 | isLoading ? ( 64 | 65 | ) : error ? ( 66 |
67 | 68 |
69 | ) : ( 70 | videos.map((video) => ( 71 | 72 | 73 | 74 | )) 75 | ) 76 | } 77 |
78 |
79 | 80 | ); 81 | }; 82 | 83 | export default SearchResults; -------------------------------------------------------------------------------- /src/HomePageContainer/SubScriptionPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState, useEffect } from 'react'; 4 | import { YOUTUBE_SEARCH_API } from '../utils/APIList'; 5 | import { NavLink, useSearchParams } from 'react-router-dom'; 6 | import CustomError from './CustomError' 7 | import ChannelVideoPage from '../Components/ChannelVideoPage'; 8 | import VideoShimmer from '../Components/ShimmerUI/VideoShimmer' 9 | import CategoryList from '../Components/CategoryList'; 10 | 11 | const SubScriptionPage = () => { 12 | 13 | const [videos, setVideos] = useState([]); 14 | const [isLoading, setIsLoading] = useState(true); 15 | const [error, setError] = useState(null); 16 | 17 | const [searchParam] = useSearchParams(); 18 | const channel = searchParam.get('cId'); 19 | 20 | useEffect(() => { 21 | const timer = setTimeout(() => { 22 | setIsLoading(false); 23 | }, 2000); 24 | 25 | return () => clearTimeout(timer); 26 | }, []); 27 | 28 | useEffect(() => { 29 | getSearchResults(); 30 | }, [channel]); 31 | 32 | const getSearchResults = async () => { 33 | try { 34 | if (!channel) return; 35 | const response = await fetch(`${YOUTUBE_SEARCH_API}&q=${channel}®ionCode=IN&type=video`); 36 | if (!response.ok) { 37 | throw new Error(`Failed to fetch search results. Status: ${response.status}`); 38 | } 39 | const data = await response.json(); 40 | setVideos(data.items || []); 41 | setError(null); 42 | } catch (error) { 43 | console.error('Error while fetching search videos', error); 44 | setError('Failed to fetch videos. Please try again later.'); 45 | } 46 | }; 47 | 48 | return ( 49 | <> 50 |
51 |
52 | 53 |
54 |
55 | { 56 | isLoading ? ( 57 | 58 | ) : error ? ( 59 |
60 | 61 |
62 | ) : ( 63 | videos.map((video) => ( 64 | 65 | 66 | 67 | )) 68 | ) 69 | } 70 |
71 |
72 | 73 | ); 74 | }; 75 | 76 | export default SubScriptionPage; -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useMemo, useState } from 'react'; 4 | import ChannelData from './WatchPageData/ChannelData'; 5 | import { useSearchParams } from 'react-router-dom' 6 | import CommentsData from './WatchPageData/CommentsData'; 7 | import LiveChat from './WatchPageData/LiveChat'; 8 | import RelatedVideoPage from './WatchPageData/RelatedVideoPage'; 9 | 10 | const WatchPage = () => { 11 | 12 | const [container, setContainer] = useState(true) 13 | // useSearchParams is used for accessing and manipulating URL parameters. 14 | const [searchParams] = useSearchParams(); 15 | const videoId = searchParams.get('v'); 16 | const videoSrc = useMemo(() => `https://www.youtube.com/embed/${videoId}?autoplay=1&mute=0`, [videoId]); 17 | 18 | return ( 19 |
20 |
21 |
22 |
23 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 |
47 |
48 | 49 | ); 50 | }; 51 | 52 | export default WatchPage; -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPageData/ChannelData.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState, useEffect } from 'react' 4 | import PropTypes from 'prop-types'; 5 | import { BiLike, BiDislike } from 'react-icons/bi' 6 | import { BiSolidLike, BiSolidDislike } from 'react-icons/bi' 7 | import { CiBellOn } from 'react-icons/ci' 8 | import { PiShareFat, PiDotsThreeBold } from 'react-icons/pi' 9 | import { MdOutlineDownloading } from 'react-icons/md' 10 | import { HiOutlineChevronDown } from 'react-icons/hi2' 11 | import { CHANNEL_INFO_API, VIDEO_DETAILS_API } from '../../utils/APIList' 12 | import { formatNumberWithSuffix, formatTime } from '../../utils/constants' 13 | 14 | const ChannelData = ({ videoId }) => { 15 | const [videoData, setVideoData] = useState({}); 16 | const [subscribe, setSubscribe] = useState(false); 17 | const [channelPicture, setChannelPicture] = useState(''); 18 | const [subScribers, setSubScribers] = useState(0) 19 | const [like, setLike] = useState(true) 20 | const [disLike, setDisLike] = useState(true) 21 | const [showMore, setShowMore] = useState(false); 22 | 23 | const subscriberCount = formatNumberWithSuffix(subScribers); 24 | const likeCount = formatNumberWithSuffix(videoData?.statistics?.likeCount); 25 | const viewCount = formatNumberWithSuffix(videoData?.statistics?.viewCount); 26 | let calender = formatTime(videoData?.snippet?.publishedAt); 27 | 28 | useEffect(() => { 29 | fetchVideoData(); 30 | }, [videoId]); 31 | 32 | const fetchVideoData = async () => { 33 | try { 34 | const data = await fetch(VIDEO_DETAILS_API + '&id=' + videoId); 35 | const response = await data.json(); 36 | setVideoData(response?.items?.[0] || {}); 37 | console.log(videoData) 38 | } catch (error) { 39 | console.log('Error while fetching video details', error); 40 | } 41 | }; 42 | 43 | useEffect(() => { 44 | if (videoData?.snippet?.channelId) { 45 | fetchChannelData(); 46 | } 47 | }, [videoData?.snippet?.channelId]); 48 | 49 | const fetchChannelData = async () => { 50 | try { 51 | const data = await fetch(CHANNEL_INFO_API + '&id=' + videoData?.snippet?.channelId); 52 | const response = await data.json(); 53 | const profilePictureUrl = response?.items?.[0]?.snippet?.thumbnails?.default?.url || ''; 54 | const subScribers = response?.items?.[0]?.statistics?.subscriberCount || '' 55 | setChannelPicture(profilePictureUrl); 56 | setSubScribers(subScribers) 57 | } catch (error) { 58 | console.log("Couldn't fetch channel data", error); 59 | } 60 | }; 61 | 62 | const handleLikeToggle = () => { 63 | setLike(!like); 64 | setDisLike(true); 65 | like ? likeCount((prev) => prev - 1) : likeCount((prev) => prev + 1); 66 | }; 67 | 68 | const handleDisLikeToggle = () => { 69 | setDisLike(!disLike); 70 | setLike(true); 71 | disLike ? likeCount((prev) => prev + 1) : likeCount((prev) => prev - 1); 72 | }; 73 | 74 | return ( 75 |
76 |
77 |

{videoData?.snippet?.title}

78 |
79 |
80 |
81 |
82 | ChannelProfile 83 |
84 | {videoData?.snippet?.channelTitle} 85 | {subscriberCount} Subscribers 86 |
87 |
88 | 103 |
104 |
105 |
106 |
107 | { 108 | like ? ( 109 | 110 | ) : ( 111 | 112 | ) 113 | } 114 | {likeCount} 115 |
116 |
117 | 118 |
119 |
120 | { 121 | disLike ? ( 122 | 123 | ) : ( 124 | 125 | ) 126 | } 127 |
128 |
129 |
130 | 131 | Share 132 |
133 |
134 | 135 | Download 136 |
137 |
138 | 139 |
140 |
141 |
142 |
143 |
144 | {viewCount} views 145 |
146 | {calender} 147 |
148 |
149 |

150 | {videoData?.snippet?.description} 151 |

152 | setShowMore(!showMore)}> 153 | { 154 | showMore ? "Show Less" : "Show More..." 155 | } 156 | 157 |
158 |
159 |
160 | ) 161 | } 162 | 163 | // 'PropTypes.string.isRequired' specifies that the 'videoId' prop must be a string and is required 164 | ChannelData.propTypes = { 165 | videoId: PropTypes.string.isRequired, 166 | }; 167 | 168 | export default ChannelData 169 | -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPageData/ChatMessage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-unused-vars */ 3 | import React from 'react' 4 | import { FaCircleUser } from "react-icons/fa6"; 5 | 6 | const ChatMessage = ({ info }) => { 7 | return ( 8 |
9 | UserImage 10 |
11 | {info.name} 12 | {info.message} 13 |
14 |
15 | ) 16 | } 17 | 18 | export default ChatMessage 19 | -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPageData/Comments.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState } from 'react'; 4 | import { BiLike, BiDislike, BiSolidLike, BiSolidDislike } from 'react-icons/bi'; 5 | import { FaCaretUp, FaCaretDown } from "react-icons/fa"; 6 | 7 | const Comments = ({ info, isReply }) => { 8 | const [showReply, setShowReply] = useState(false); 9 | const [like, setLike] = useState(false); 10 | const [dislike, setDislike] = useState(false); 11 | const [likeCount, setLikeCount] = useState(0); 12 | 13 | const getComments = (item, isReply) => { 14 | return isReply ? item.snippet : item.snippet.topLevelComment.snippet; 15 | }; 16 | 17 | const handleLike = () => { 18 | setLike(!like); 19 | setDislike(false); 20 | if (!like) { 21 | setLikeCount(likeCount + 1); 22 | } else { 23 | setLikeCount(likeCount - 1); 24 | } 25 | }; 26 | 27 | const handleDislike = () => { 28 | setDislike(!dislike); 29 | setLike(false); 30 | if (!dislike && likeCount > 0) { 31 | setLikeCount(likeCount - 1); 32 | } 33 | }; 34 | 35 | const commentItem = getComments(info, isReply); 36 | 37 | return ( 38 |
39 |
40 |
41 | Profile 46 |
47 |
48 |

{commentItem?.authorDisplayName}

49 |

{commentItem?.textOriginal}

50 |
51 |
52 |
53 |
54 |
55 |
56 | 59 | {likeCount > 0 && likeCount} 60 |
61 | 64 |
65 |
66 | Reply 67 |
68 |
69 |
70 | {info.replies && ( 71 | <> 72 |

setShowReply(!showReply)} 75 | > 76 | {showReply ? ( 77 |
78 | Hide replies 79 | 80 |
81 | ) : ( 82 |
83 | Show replies 84 | 85 |
86 | )} 87 |

88 | {showReply && info.replies.comments && info.replies.comments.map((item) => ( 89 | 90 | ))} 91 | 92 | )} 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Comments; 100 | -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPageData/CommentsData.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-unused-vars */ 4 | import React, { useEffect, useState } from 'react' 5 | import { BiMenuAltLeft } from "react-icons/bi"; 6 | import { YOUTUBE_COMMENTS_API } from '../../utils/APIList'; 7 | import Comments from './Comments'; 8 | 9 | const CommentsData = ({ videoId }) => { 10 | const [comments, setComments] = useState([]) 11 | 12 | useEffect(() => { 13 | fetchComments() 14 | }, []) 15 | 16 | const fetchComments = async () => { 17 | try { 18 | const data = await fetch(`${YOUTUBE_COMMENTS_API}&order=relevance&videoId=${videoId}`); 19 | const response = await data.json(); 20 | if (response?.items) { 21 | setComments(response.items); 22 | } else { 23 | console.error('No comments found in the API response'); 24 | } 25 | } catch (error) { 26 | console.error("Error while fetching comments", error); 27 | } 28 | }; 29 | 30 | return ( 31 |
32 |
33 | {comments.length} Comments 34 |
35 | 36 | Sort by 37 |
38 |
39 |
40 |
    41 | { 42 | comments.map((comment, index) => ( 43 | 44 | )) 45 | } 46 |
47 |
48 |
49 | ) 50 | } 51 | 52 | export default CommentsData 53 | -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPageData/LiveChat.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useEffect, useState } from 'react'; 4 | import ChatMessage from './ChatMessage'; 5 | import { useDispatch, useSelector } from 'react-redux' 6 | import { AvatarGenerator } from 'random-avatar-generator' 7 | import { addMessage } from '../../utils/chatSlice'; 8 | import { generateRandomComment, generateRandomName } from '../../utils/helper'; 9 | import MyPic from '../../assets/MyPic.jpg'; 10 | import { VscSend } from 'react-icons/vsc'; 11 | import { FaRegFaceSmile } from "react-icons/fa6"; 12 | 13 | const LiveChat = () => { 14 | const [message, setMessage] = useState(''); 15 | const [isFocused, setIsFocused] = useState(false); 16 | const [isChatVisible, setIsChatVisible] = useState(true); 17 | const dispatch = useDispatch(); 18 | const chatMessage = useSelector((store) => store.chat.messages); 19 | 20 | const InputStyle = `relative md:w-[92.5vw] lg:w-[28.4vw] max-sm:w-[100%] outline-none shadow-gray-400 shadow-2xl py-1 text-sm mx-[9px] border-b-2 ${isFocused ? 'border-blue-700 transition-all duration-300 px-9' : 'border-gray-500'} text-gray-600 bg-gray-100` 21 | 22 | const ChatBoxStyle = 'shadow-gray-300 shadow-2xl md:h-[50vh] md:w-[92.5vw] lg:w-[30vw] lg:h-[58vh] max-sm:h-[50vh] bg-gray-100 flex flex-col-reverse md:overflow-y-scroll overflow-x-hidden chatScroll' 23 | 24 | let count = 0; 25 | const generator = new AvatarGenerator(); 26 | generator.generateRandomAvatar(); 27 | 28 | useEffect(() => { 29 | const timer = setInterval(() => { 30 | // API Polling 31 | dispatch( 32 | addMessage({ 33 | imgUrl: `${generator.generateRandomAvatar(count) 34 | }`, 35 | name: generateRandomName(), 36 | message: generateRandomComment() + "...✨", 37 | }) 38 | ); 39 | count++; 40 | }, 2000); 41 | 42 | return () => clearInterval(timer); 43 | }, []); 44 | 45 | const handleChat = (e) => { 46 | e.preventDefault(); 47 | 48 | dispatch( 49 | addMessage({ 50 | imgUrl: MyPic, 51 | name: 'Omkar Karale', 52 | message: message, 53 | }) 54 | ); 55 | setMessage(''); 56 | }; 57 | 58 | const toggleChatVisibility = () => { 59 | setIsChatVisible(!isChatVisible); 60 | }; 61 | 62 | useEffect(() => { 63 | const handleResize = () => { 64 | setIsChatVisible(window.innerWidth >= 768); // Set to false for screen width less than 768px 65 | }; 66 | handleResize(); 67 | window.addEventListener('resize', handleResize); 68 | 69 | return () => window.removeEventListener('resize', handleResize); 70 | 71 | }, []); 72 | 73 | return ( 74 |
75 |
76 | Live Chat 77 |
78 | { 79 | isChatVisible && ( 80 |
81 | {chatMessage.map((chat, index) => ( 82 | 83 | ))} 84 |
85 | ) 86 | } 87 |
88 |
89 |
90 |
91 |
92 | Mypic 93 |
94 |
95 | Omkar Karale 96 |
97 |
98 |
😊
99 |
100 |
101 |
e.preventDefault()} className='flex items-center'> 102 | { 103 | isFocused && ( 104 | 105 | 106 | 107 | ) 108 | } 109 | setMessage(e.target.value)} 115 | onFocus={() => setIsFocused(true)} 116 | onBlur={() => setIsFocused(false)} 117 | /> 118 | 122 |
123 |
124 |
125 |
126 |
128 | 131 |
132 |
133 | ); 134 | }; 135 | 136 | export default LiveChat; 137 | -------------------------------------------------------------------------------- /src/HomePageContainer/WatchPageData/RelatedVideoPage.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useEffect, useState } from 'react' 4 | import RelatedVideos from '../../Components/RelatedVideos' 5 | import { NavLink } from 'react-router-dom' 6 | import { YOUTUBE_VIDEO_API } from '../../utils/APIList' 7 | 8 | const RelatedVideoPage = () => { 9 | 10 | const [videos, setVideos] = useState([]) 11 | const [loading, setLoading] = useState(true); 12 | 13 | useEffect(() => { 14 | // Simulate loading data 15 | const timer = setTimeout(() => { 16 | setLoading(false); 17 | }, 2000); 18 | 19 | return () => clearTimeout(timer); 20 | }, []); 21 | 22 | useEffect(() => { 23 | getRelatedVideos() 24 | }) 25 | 26 | const getRelatedVideos = async () => { 27 | try { 28 | const data = await fetch(YOUTUBE_VIDEO_API) 29 | if (!data.ok) { 30 | throw new Error(`Failed to fetch videos. Status: ${data.status}`); 31 | } 32 | const response = await data.json() 33 | setVideos(response?.items) 34 | console.log(response?.items) 35 | } catch (error) { 36 | console.error('Error while fetching the videos:', error); 37 | } 38 | } 39 | 40 | return ( 41 |
42 | { 43 | videos.map((video) => ( 44 | 45 | 46 | 47 | )) 48 | } 49 |
50 | ) 51 | } 52 | 53 | export default RelatedVideoPage 54 | -------------------------------------------------------------------------------- /src/assets/MyPic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsOnkar-dev/YouTube-UI/edfd7bf57595433fa5baa4909da2c07bd0d94be0/src/assets/MyPic.jpg -------------------------------------------------------------------------------- /src/assets/YouTubeWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsOnkar-dev/YouTube-UI/edfd7bf57595433fa5baa4909da2c07bd0d94be0/src/assets/YouTubeWhite.png -------------------------------------------------------------------------------- /src/assets/YouTube_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsOnkar-dev/YouTube-UI/edfd7bf57595433fa5baa4909da2c07bd0d94be0/src/assets/YouTube_Logo.png -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | font-family: "Ubuntu"; 10 | } 11 | 12 | body { 13 | overflow-x: hidden; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | /* body::-webkit-scrollbar-track { 19 | cursor: pointer; 20 | } */ 21 | 22 | body::-webkit-scrollbar { 23 | width: 8px; 24 | cursor: pointer; 25 | } 26 | 27 | body::-webkit-scrollbar-thumb { 28 | background: rgb(92, 90, 90); 29 | height: 50px !important; 30 | } 31 | 32 | .sidebar::-webkit-scrollbar-track { 33 | border-radius: 12px; 34 | background-color: white; 35 | } 36 | 37 | .sidebar::-webkit-scrollbar { 38 | width: 0; 39 | background-color: white; 40 | } 41 | 42 | .sidebar:hover::-webkit-scrollbar { 43 | width: 6px; 44 | } 45 | 46 | .sidebar::-webkit-scrollbar-thumb { 47 | background-color: rgb(125, 123, 123); 48 | border-radius: 12px; 49 | } 50 | 51 | .scrollBar::-webkit-scrollbar { 52 | width: 0.8rem; 53 | height: 0.8rem; 54 | } 55 | 56 | .scrollBar::-webkit-scrollbar-thumb { 57 | display: none; 58 | } 59 | 60 | .sidebar-open { 61 | animation: slideIn 0.2s ease-in-out forwards; 62 | } 63 | 64 | @keyframes slideIn { 65 | 0% { 66 | left: -100%; 67 | } 68 | 100% { 69 | left: 0; 70 | } 71 | } 72 | 73 | .blur-effect { 74 | filter: blur(2px); 75 | -webkit-filter: blur(2px); 76 | pointer-events: none; 77 | } 78 | 79 | .chatScroll::-webkit-scrollbar-track { 80 | border-radius: 12px; 81 | } 82 | 83 | .chatScroll::-webkit-scrollbar { 84 | width: 0; 85 | background-color: rgb(231, 227, 227); 86 | } 87 | 88 | .chatScroll:hover::-webkit-scrollbar { 89 | width: 7px; 90 | } 91 | 92 | .chatScroll::-webkit-scrollbar-thumb { 93 | background-color: rgb(135, 133, 133); 94 | border-radius: 12px; 95 | } 96 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/utils/APIList.jsx: -------------------------------------------------------------------------------- 1 | const API_KEY = import.meta.env.VITE_GOOGLE_API_KEY; 2 | // Vite uses the 'import.meta.env' object instead of 'process.env' to access environment variables. 3 | 4 | if (!API_KEY) { 5 | console.error("API key is missing. Make sure you have set up the environment variable."); 6 | } 7 | 8 | export const YOUTUBE_VIDEO_API = "https://youtube.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&chart=mostPopular&maxResults=50®ionCode=IN&key=" + API_KEY; 9 | 10 | export const YOUTUBE_SEARCH_SUGGESTION_API = 11 | "https://api.codetabs.com/v1/proxy?quest=" + encodeURIComponent("https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q="); 12 | // 'https://thingproxy.freeboard.io/fetch/http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&' 13 | // "https://corsproxy.org/?https%3A%2F%2Fsuggestqueries.google.com%2Fcomplete%2Fsearch%3Fclient%3Dfirefox%26ds%3Dyt%26" 14 | 15 | export const CHANNEL_INFO_API = "https://youtube.googleapis.com/youtube/v3/channels?part=snippet%2CcontentDetails%2Cstatistics&key=" + API_KEY 16 | 17 | export const VIDEO_DETAILS_API = 18 | "https://youtube.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&key=" + 19 | API_KEY; 20 | 21 | export const YOUTUBE_SEARCH_API = "https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=50&q=&key=" + API_KEY; 22 | 23 | export const YOUTUBE_SEARCHCATEGORY_API = "https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=50&type=video&q=&key=" + API_KEY; 24 | 25 | export const fetchTagsUrl = 26 | "https://youtube.googleapis.com/youtube/v3/videoCategories?part=snippet®ionCode=IN&key=" + 27 | API_KEY; 28 | 29 | export const YOUTUBE_COMMENTS_API = `https://youtube.googleapis.com/youtube/v3/commentThreads?part=snippet%2Creplies&maxResults=50&videoId=&key=${API_KEY}` 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/utils/appSlice.jsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const appSlice = createSlice({ 4 | name: 'app', 5 | initialState: { 6 | isMenuOpen: false, 7 | }, 8 | reducers: { 9 | toggleMenu: (state) => { 10 | state.isMenuOpen = !state.isMenuOpen 11 | }, 12 | }, 13 | }) 14 | 15 | export const { toggleMenu } = appSlice.actions 16 | export default appSlice.reducer -------------------------------------------------------------------------------- /src/utils/chatSlice.jsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { LIVE_CHAT_COUNT } from "./constants"; 3 | 4 | const chatSlice = createSlice({ 5 | name: 'chat', 6 | initialState: { 7 | messages: [] 8 | }, 9 | reducers: { 10 | addMessage: (state, action) => { 11 | state.messages.splice(LIVE_CHAT_COUNT, 1) 12 | state.messages.unshift(action.payload) 13 | // updates the state by pushing the payload property of the action object into the messages array in the state. 14 | }, 15 | }, 16 | }) 17 | // Overall, this reducer function handles the action of adding a new message to the state. When dispatched, it appends the new message to the existing list of messages in the state. 18 | 19 | export const { addMessage } = chatSlice.actions 20 | export default chatSlice.reducer -------------------------------------------------------------------------------- /src/utils/constants.jsx: -------------------------------------------------------------------------------- 1 | import { MdHomeFilled } from 'react-icons/md'; 2 | import { MdSubscriptions } from 'react-icons/md'; 3 | import { SiYoutubeshorts } from 'react-icons/si'; 4 | import { IoMdTrendingUp } from "react-icons/io"; 5 | import { HiShoppingBag } from "react-icons/hi2"; 6 | import { MdMusicNote } from "react-icons/md"; 7 | import { BiSolidMoviePlay } from "react-icons/bi"; 8 | import { RiLiveFill } from "react-icons/ri"; 9 | import { SiYoutubegaming } from "react-icons/si"; 10 | import { PiStudentFill } from "react-icons/pi"; 11 | import { SiShopify } from "react-icons/si"; 12 | import { GiTrophy } from "react-icons/gi"; 13 | import { SiGooglenews } from "react-icons/si"; 14 | import { FaYoutube } from "react-icons/fa"; 15 | import { SiYoutubestudio } from "react-icons/si"; 16 | import { SiYoutubemusic } from "react-icons/si"; 17 | import { TfiYoutube } from "react-icons/tfi"; 18 | import { IoSettingsSharp } from "react-icons/io5"; 19 | import { RiChatHistoryFill } from "react-icons/ri"; 20 | import { IoMdHelpCircle } from "react-icons/io"; 21 | import { MdFeedback } from "react-icons/md"; 22 | 23 | export const Home = [ 24 | { 25 | icon: , 26 | name: "Home" 27 | }, 28 | { 29 | icon: , 30 | name: "Subscriptions" 31 | }, 32 | { 33 | icon: , 34 | name: "Shorts" 35 | }, 36 | ] 37 | 38 | export const Subscriptions = [ 39 | { 40 | src: "https://yt3.googleusercontent.com/eu051krrRNQMMi5h6ynfnvhFJzxzSKulJQ42g5v72MQ9Bvv8KdpNIa6yM-0iGpnDgSF0itAD=s176-c-k-c0x00ffffff-no-rj", 41 | profileId: "UCpEhnqL0y41EpW2TvWAHD7Q", 42 | fullname: "SET India", 43 | }, 44 | { 45 | src: "https://yt3.googleusercontent.com/ytc/AOPolaSj48pypV9ilqNUztYjQ8Q760NYCAw3w1LwoWbJYQ=s176-c-k-c0x00ffffff-no-rj", 46 | profileId: "UC3N9i_KvKZYP4F84FPIzgPQ", 47 | fullname: "Akshay Saini", 48 | }, 49 | { 50 | src: "https://yt3.googleusercontent.com/XE7Iq8jvJ07ptMc-HxZR_V-2XgXCb0i06i4E_dypl7xSR655WXaQeglfqNuEeuwH3oM9RKVodQ=s176-c-k-c0x00ffffff-no-rj", 51 | profileId: "UCq-Fj5jknLsUf-MWSy4_brA", 52 | fullname: "T-Series", 53 | }, 54 | { 55 | src: "https://yt3.googleusercontent.com/ytc/AIf8zZTDkajQxPa4sjDOW-c3er1szXkSAO-H9TiF4-8u_Q=s176-c-k-c0x00ffffff-no-rj", 56 | profileId: "UC8butISFwT-Wl7EV0hUK0BQ", 57 | fullname: "freeCodeCamp", 58 | }, 59 | { 60 | src: "https://yt3.googleusercontent.com/l_ZIXrVEQcHTBTsmpt2CFiWJF9_0hwB3rngr1_lxozZ3Lz58Ij5TcDFOp2TYlioU2gI9RlyExw=s176-c-k-c0x00ffffff-no-rj", 61 | profileId: "UCqwUrj10mAEsqezcItqvwEw", 62 | fullname: "BB Ki Vines", 63 | }, 64 | { 65 | src: "https://yt3.googleusercontent.com/ytc/AIf8zZTPIL9EkFafJj8HqxgEj9avK-OBX-U6p0V9NrEwSw=s176-c-k-c0x00ffffff-no-rj", 66 | profileId: "UC0RhatS1pyxInC00YKjjBqQ", 67 | fullname: "GeeksforGeeks", 68 | }, 69 | { 70 | src: "https://yt3.googleusercontent.com/bFpwiiOB_NLCVsIcVQ9UcwBjb1RzipnMmtNfLSWpeIaHboyGkBCq4KBitmovRbStk9WvIWIZOyo=s176-c-k-c0x00ffffff-no-rj", 71 | profileId: "UCIEv3lZ_tNXHzL3ox-_uUGQ", 72 | fullname: "Gordon Ramsay", 73 | }, 74 | ]; 75 | 76 | export const Explore = [ 77 | { 78 | icon: , 79 | name: "Trending" 80 | }, 81 | { 82 | icon: , 83 | name: "Shopping" 84 | }, 85 | { 86 | icon: , 87 | name: "Music" 88 | }, 89 | { 90 | icon: , 91 | name: "Movies" 92 | }, 93 | { 94 | icon: , 95 | name: "Live" 96 | }, 97 | { 98 | icon: , 99 | name: "Sports" 100 | }, 101 | { 102 | icon: , 103 | name: "News" 104 | }, 105 | { 106 | icon: , 107 | name: "Gaming" 108 | }, 109 | { 110 | icon: , 111 | name: "Learning" 112 | }, 113 | { 114 | icon: , 115 | name: "Fashion & Beauty" 116 | }, 117 | ] 118 | 119 | 120 | export const Premium = [ 121 | { 122 | icon: , 123 | name: "YouTube Premium" 124 | }, 125 | { 126 | icon: , 127 | name: "YouTube Studio" 128 | }, 129 | { 130 | icon: , 131 | name: "YouTube Music" 132 | }, 133 | { 134 | icon: , 135 | name: "YouTube Kids" 136 | }, 137 | ] 138 | 139 | export const Setting = [ 140 | { 141 | icon: , 142 | name: "Settings" 143 | }, 144 | { 145 | icon: , 146 | name: "Report History" 147 | }, 148 | { 149 | icon: , 150 | name: "Help" 151 | }, 152 | { 153 | icon: , 154 | name: "Send Feedback" 155 | }, 156 | ] 157 | 158 | export const TagNames = [ 159 | "All", 160 | "Gaming", 161 | "Mixes", 162 | "React JS", 163 | "Freecodecamp", 164 | "Fortnite", 165 | "Namaste JavaScript", 166 | "Rohit Sharma", 167 | "Comedy", 168 | "T-series", 169 | "Thrillers", 170 | "Indian Premier League", 171 | "Programming", 172 | "Dramedy", 173 | "Cricket", 174 | "Football", 175 | "News", 176 | "JavaScript", 177 | "Prodcasts", 178 | "Comedy Clubs", 179 | "Data Structures", 180 | ]; 181 | 182 | export const LIVE_CHAT_COUNT = 25; 183 | 184 | export const formatTime = (time) => { 185 | let currentDate = new Date(); 186 | let inputDate = new Date(time); 187 | let differenceInMilliseconds = currentDate - inputDate; 188 | let differenceInDays = differenceInMilliseconds / (24 * 60 * 60 * 1000); 189 | 190 | if (differenceInDays > 30) { 191 | let monthsAgo = Math.ceil(differenceInDays / 30.44); 192 | 193 | if (monthsAgo === 1) { 194 | return {monthsAgo} Month ago; 195 | } else { 196 | return {monthsAgo} Months ago; 197 | } 198 | } else { 199 | let daysAgo = Math.ceil(differenceInDays); 200 | 201 | if (daysAgo === 1) { 202 | return {daysAgo} day ago; 203 | } else { 204 | return {daysAgo} days ago; 205 | } 206 | } 207 | }; 208 | 209 | 210 | export const timeDuration = (isoDuration) => { 211 | let hours = 0; 212 | let minutes = 0; 213 | let seconds = 0; 214 | let minutesIndex; 215 | let hoursIndex; 216 | 217 | if (typeof isoDuration !== 'string' || !isoDuration.trim()) { 218 | return '00:00'; // Default value when isoDuration is not valid 219 | } 220 | 221 | if (isoDuration.includes("H")) { 222 | hoursIndex = isoDuration.indexOf("H"); 223 | hours = parseInt(isoDuration.slice(2, hoursIndex)); 224 | } 225 | 226 | if (!hoursIndex) { 227 | hoursIndex = 1; 228 | } 229 | 230 | if (isoDuration.includes("M")) { 231 | minutesIndex = isoDuration.indexOf("M"); 232 | minutes = parseInt(isoDuration.slice(hoursIndex + 1, minutesIndex)); 233 | } 234 | if (!minutesIndex) { 235 | minutesIndex = 1; 236 | } 237 | const secondsIndex = isoDuration.indexOf("S"); 238 | if (secondsIndex !== -1) { 239 | seconds = parseInt(isoDuration.slice(minutesIndex + 1, secondsIndex)); 240 | } 241 | 242 | // Format the time 243 | if (!hours) { 244 | const formattedTime = `${padZero(minutes)}:${padZero(seconds)}`; 245 | return formattedTime; 246 | } else { 247 | const formattedTime = `${padZero(hours)}:${padZero(minutes)}:${padZero( 248 | seconds 249 | )}`; 250 | return formattedTime; 251 | } 252 | } 253 | 254 | // Function to pad single-digit numbers with leading zeros 255 | function padZero(num) { 256 | return num < 10 ? `0${num}` : num; 257 | } 258 | 259 | export const formatNumberWithSuffix = (x) => { 260 | if (x === undefined) { 261 | return 0; 262 | } 263 | if (x >= 1000000) { 264 | return (x / 1000000).toFixed(1) + "M"; 265 | } else if (x >= 1000) { 266 | return (x / 1000).toFixed(1) + "K"; 267 | } 268 | return x.toString(); 269 | }; 270 | -------------------------------------------------------------------------------- /src/utils/helper.jsx: -------------------------------------------------------------------------------- 1 | var nameList = [ 2 | "Josie Suarez", 3 | "Soren Golden", 4 | "Giuliana Maldonado", 5 | "Javier Aguirre", 6 | "Ariah Ortiz", 7 | "Landon Cabrera", 8 | "Daleyza Huff", 9 | "Finnley Fitzpatrick", 10 | "Annabella Randall", 11 | "Trenton Barry", 12 | "Waverly Ahmed", 13 | "Harry Watkins", 14 | "Lola Campbell", 15 | "Christopher Cain", 16 | "Kendra Jones", 17 | "William May", 18 | "Adriana Sanders", 19 | "Jose Franklin", 20 | "Angela Villalobos", 21 | "Reuben Guerra", 22 | "Edith Nash", 23 | "Chandler Browning", 24 | "Princess Sweeney", 25 | "Nixon Santana", 26 | "Myra Weeks", 27 | "Anders Parsons", 28 | "Maia Robbins", 29 | "Finnegan Baxter", 30 | "Lara Truong", 31 | "Ayan Conner", 32 | "Alondra Knox", 33 | "Valentin Cain", 34 | "Kendra Person", 35 | "Moses Pineda", 36 | "Nola Brady", 37 | "Reed Anthony", 38 | "Macy Lim", 39 | "Cal Waller", 40 | "Whitley Barajas", 41 | "Brennan Roberts", 42 | "Paisley Golden", 43 | "Ryann Valdez", 44 | "Kyler Morgan", 45 | "Delilah Kirk", 46 | "Alessandro Orr", 47 | "Alaiya Vasquez", 48 | "Rowan Ramos", 49 | "Alice Holloway", 50 | "Sutton Cabrera", 51 | "Daleyza Kaur", 52 | "Augustine Parsons", 53 | "Maia Gregory", 54 | "Travis Montgomery", 55 | "Evangeline Warner", 56 | "Jaxton McGee", 57 | "Kayleigh Velez", 58 | "Kareem Silva", 59 | "Lucia Delarosa", 60 | "Osiris Hernandez", 61 | "Camila Lim", 62 | "Cal Potter", 63 | "Rory Townsend", 64 | "Alexis Ortega", 65 | "Lilah Clay", 66 | "Yosef Heath", 67 | "Amani Henderson", 68 | "Beau Collins", 69 | "Kinsley Davenport", 70 | "Dariel Bartlett", 71 | "Aubrielle Hayden", 72 | "Leroy Ponce", 73 | "Aileen Barrera", 74 | "Makai Grant", 75 | "Alaina Harrell", 76 | "Nelson Madden", 77 | "Violette Winters", 78 | "Deandre Hess", 79 | "Kaliyah Gibbs", 80 | "Deacon Snow", 81 | "Alexia Garrison", 82 | "Noe Washington", 83 | "Valerie McDaniel", 84 | "Major Weber", 85 | "Alayah Harris", 86 | "Samuel McDowell", 87 | "Rayna Thomas", 88 | "Logan Sexton", 89 | "Ellen Conner", 90 | "Phillip Norman", 91 | "Malani Harrison", 92 | "Gavin Grant", 93 | "Alaina Hess", 94 | "Lawrence Phan", 95 | "Elsa Woodward", 96 | "Jeremias Jennings", 97 | "Palmer Hoffman", 98 | "Steven Hodges", 99 | "Eve Watson", 100 | "Greyson McDaniel", 101 | "Dahlia Greer", 102 | "Koda Nash", 103 | "Novah Lynch", 104 | "Zane Rivera", 105 | "Lillian Hodges", 106 | "Alonzo Lara", 107 | "Heidi Taylor", 108 | "Jackson Knox", 109 | "Kallie Ruiz", 110 | "Austin Guerra", 111 | "Edith McCullough", 112 | "Briar Gross", 113 | "Angel Watts", 114 | "Dakota McClure", 115 | "Estella Warren", 116 | "Abel Flores", 117 | "Emilia Norris", 118 | "Cairo Poole", 119 | "Bonnie Medina", 120 | "George Murphy", 121 | "Bella Wood", 122 | "Carson Bradley", 123 | "Vanessa Richards", 124 | "Holden Howard", 125 | "Sophie Ware", 126 | "Tadeo Wallace", 127 | "Arianna Palmer", 128 | "Theo Conner", 129 | "Alondra Bush", 130 | "Tyson Clay", 131 | "Aliana Yang", 132 | "Malcolm Foley", 133 | "Zaylee Arias", 134 | "Alec Herrera", 135 | "Ximena Best", 136 | "Harlem Rowe", 137 | "Matilda Kerr", 138 | "Louie Jordan", 139 | "Adalynn Arroyo", 140 | "Alberto Knox", 141 | "Kallie Schroeder", 142 | "Izaiah Olsen", 143 | "Oaklyn Carson", 144 | "Ares Burke", 145 | "Vera Estes", 146 | "Hakeem Middleton", 147 | "Madalyn Gillespie", 148 | "Forest Farrell", 149 | "Kassidy Solis", 150 | "Ronin Cortez", 151 | "Haven Snyder", 152 | "Thiago Buckley", 153 | ]; 154 | 155 | const comments = [ 156 | "Hey everyone, thanks for joining the chat!", 157 | "Let me know if you have any questions?", 158 | "I'm learning so much from this stream.", 159 | "Thanks for sharing your knowledge!", 160 | "I'm so glad I found this stream.", 161 | "Thanks for making this stream so interactive.", 162 | "I'm so glad I subscribed to your channel.", 163 | "I always enjoy your streams.", 164 | "Thanks for the giveaway, I'm so excited to win!", 165 | "I'm really looking forward to your next stream!", 166 | "So helpful!", 167 | "Amazing!", 168 | "Love it!", 169 | "Spot on!", 170 | "Exactly what I needed!", 171 | "Hey, great stream! I'm really enjoying it.", 172 | "Thanks for answering my question!", 173 | "I love your content! Keep up the great work.", 174 | "This is so interesting! I'm learning a lot.", 175 | "I'm having a lot of fun! Thanks for streaming.", 176 | "You're so funny! I'm laughing so hard.", 177 | "You're so talented! I'm amazed.", 178 | "I'm so glad I found your channel.", 179 | "I'm really looking forward to your next stream!", 180 | "Thanks for the giveaway! I'm so excited to win.", 181 | "I'm having a great time in the chat!", 182 | "I'm so glad to be a part of this community.", 183 | "Thanks for making me feel welcome.", 184 | "I'm so happy to be here.", 185 | "I'm having a blast!", 186 | "This is the best stream ever!", 187 | "Brilliant!", 188 | "You nailed it!", 189 | "Your content always brightens my day!", 190 | "I'm hooked on your channel. Can't stop watching!", 191 | "Big fan here! Keep inspiring us.", 192 | "I've been binge-watching your videos all week!", 193 | "Your positivity is contagious.", 194 | "Just joined the stream. What did I miss?", 195 | "I'm amazed by your creativity.", 196 | "Your channel is my go-to for inspiration.", 197 | "Shoutout to the amazing community here!", 198 | "I've learned so much from your videos. Thank you!", 199 | "Your energy is infectious. Love tuning in.", 200 | "Let's keep the chat positive and respectful!", 201 | "I've shared your channel with all my friends.", 202 | "You're making a difference with your content.", 203 | "I appreciate the effort you put into making it.", 204 | "Can't wait to see what's next on your channel!", 205 | "Sending virtual hugs to everyone in the chat!", 206 | "Short and sweet, love it!", 207 | "This is gold!", 208 | "Perfect!", 209 | "Thanks a bunch!", 210 | "Mind blown!", 211 | "Great content! Keep it up!", 212 | "I love this channel!", 213 | "Awesome stream! I'm enjoying it a lot.", 214 | "Inspiring content.Keep it coming!", 215 | "Your videos motivate me daily.", 216 | "You're a positive force online.", 217 | "Grateful for your uplifting messages.", 218 | "Your channel spreads joy effortlessly.", 219 | "Impressive content. Well done!", 220 | "You bring smiles to many faces.", 221 | "Your channel brightens my day.", 222 | "You're making a difference, truly.", 223 | "Your authenticity shines through.", 224 | "Refreshing content. Love it!", 225 | "You're a ray of sunshine.", 226 | "Positive vibes all the way!", 227 | "You're a role model for many.", 228 | "Thanks for being so genuine.", 229 | "Love your creative approach.", 230 | "Your passion is inspiring.", 231 | "Your positivity is contagious.", 232 | "You make the internet better.", 233 | "You're a source of inspiration.", 234 | "Your videos are impactful.", 235 | "You make learning enjoyable.", 236 | "Appreciate your valuable insights.", 237 | "You're a true content creator.", 238 | "Your dedication is admirable.", 239 | "Your channel brings joy to many.", 240 | "Thanks for sharing your wisdom.", 241 | "Your energy is infectious!", 242 | "You create smiles effortlessly.", 243 | "Your presence is uplifting.", 244 | "Hi everyone! How's everyone doing today?", 245 | "This is so interesting! I'm learning a lot.", 246 | "Shoutout to the amazing moderators!", 247 | "Can't wait for the next video!", 248 | "Greetings from [Your Location]!", 249 | "I just subscribed! Your channel is amazing.", 250 | "Thanks for the shoutout! You made my day.", 251 | "Can't get enough!", 252 | "Incredible!", 253 | "You rock!", 254 | "Genius!", 255 | "Spectacular!", 256 | "Top-notch!", 257 | "You make learning enjoyable, thank you for that!", 258 | "Your information and content is excellent", 259 | 260 | ] 261 | 262 | export const truncateText = (text, maxLength) => { 263 | if (text.length <= maxLength) { 264 | return text; 265 | } else { 266 | return text.slice(0, maxLength) + "..."; 267 | } 268 | }; 269 | 270 | export const generateRandomName = () => { 271 | var finalName = nameList[Math.floor(Math.random() * nameList.length)]; 272 | return finalName 273 | } 274 | 275 | export const generateRandomComment = () => { 276 | const randomComment = comments[Math.floor(Math.random() * comments.length)]; 277 | return randomComment; 278 | } 279 | 280 | // Generating Random chat for Live Chat 281 | // export function generateRandomComment(length) { 282 | // let result = ""; 283 | // const characters = 284 | // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 285 | // const charactersLength = characters.length; 286 | // let counter = 0; 287 | // while (counter < length) { 288 | // result += characters.charAt(Math.floor(Math.random() * charactersLength)); 289 | // counter += 1; 290 | // } 291 | // return result; 292 | // } -------------------------------------------------------------------------------- /src/utils/searchSlice.jsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const searchSlice = createSlice({ 4 | name: 'search', 5 | initialState: { 6 | 7 | }, 8 | reducers: { 9 | cacheResults: (state, action) => { 10 | // Using Object.assign to merge the current state with the payload just like spread in ES6 11 | // state = {...state, ...action.payload} 12 | state = Object.assign(state, action.payload) 13 | } 14 | } 15 | }) 16 | 17 | export const { cacheResults } = searchSlice.actions 18 | export default searchSlice.reducer -------------------------------------------------------------------------------- /src/utils/store.jsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import appSlice from './appSlice'; 3 | import searchSlice from './searchSlice'; 4 | import chatSlice from './chatSlice'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | app: appSlice, 9 | search: searchSlice, 10 | chat: chatSlice, 11 | }, 12 | }) 13 | 14 | export default store -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /** @type {import('tailwindcss').Config} */ 3 | export default { 4 | content: [ 5 | './index.html', 6 | './src/**/*.{js,ts,jsx,tsx}' 7 | ], 8 | theme: { 9 | extend: { 10 | 11 | } 12 | }, 13 | plugins: [ require('tailwind-scrollbar')] 14 | } 15 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import dotenv from 'dotenv' 4 | 5 | dotenv.config() 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [react()] 10 | }) 11 | --------------------------------------------------------------------------------