├── .env.example ├── .env ├── public ├── favicon.ico └── index.html ├── src ├── index.js ├── components │ ├── Loader.jsx │ ├── Navbar.jsx │ ├── index.js │ ├── Videos.jsx │ ├── SearchFeed.jsx │ ├── Sidebar.jsx │ ├── SearchBar.jsx │ ├── ChannelDetail.jsx │ ├── Feed.jsx │ ├── VideoCard.jsx │ ├── ChannelCard.jsx │ └── VideoDetail.jsx ├── utils │ ├── fetchFromAPI.js │ └── constants.js ├── App.js └── index.css ├── .gitignore ├── README.md ├── Dockerfile ├── deployment.yml └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_RAPID_API_KEY = 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_RAPID_API_KEY=KJwZZIJSFimshuivMSVGaiYzkRomp15f2vKjsnK4bKzuUzVLzA -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aj7Ay/Youtube-clone-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | import './index.css'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | 9 | root.render(); 10 | -------------------------------------------------------------------------------- /src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, CircularProgress, Stack } from '@mui/material'; 3 | 4 | const Loader = () => ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default Loader; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/utils/fetchFromAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const BASE_URL = 'https://youtube-v31.p.rapidapi.com'; 4 | 5 | const options = { 6 | params: { 7 | maxResults: 50, 8 | }, 9 | headers: { 10 | 'X-RapidAPI-Key': process.env.REACT_APP_RAPID_API_KEY, 11 | 'X-RapidAPI-Host': 'youtube-v31.p.rapidapi.com', 12 | }, 13 | }; 14 | 15 | export const fetchFromAPI = async (url) => { 16 | const { data } = await axios.get(`${BASE_URL}/${url}`, options); 17 | 18 | return data; 19 | }; 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build and Deploy a Modern YouTube Clone Application in React JS with Material UI 5 2 | 3 | ![YouTube](https://i.ibb.co/4R5RkmW/Thumbnail-5.png) 4 | 5 | ### Showcase your dev skills with practical experience and land the coding career of your dreams 6 | 💻 JS Mastery Pro - https://jsmastery.pro/youtube 7 | ✅ A special YOUTUBE discount code is automatically applied! 8 | 9 | 📙 Get the Ultimate Frontend & Backend Development Roadmaps, a Complete JavaScript Cheatsheet, Portfolio Tips, and more - https://www.jsmastery.pro/links 10 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "@mui/material"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { logo } from "../utils/constants"; 5 | import { SearchBar } from "./"; 6 | 7 | const Navbar = () => ( 8 | 9 | 10 | logo 11 | 12 | 13 | 14 | ); 15 | 16 | export default Navbar; 17 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as ChannelCard } from './ChannelCard'; 2 | export { default as VideoCard } from './VideoCard'; 3 | export { default as ChannelDetail } from './ChannelDetail'; 4 | export { default as Feed } from './Feed'; 5 | export { default as Loader } from './Loader'; 6 | export { default as Navbar } from './Navbar'; 7 | export { default as SearchBar } from './SearchBar'; 8 | export { default as VideoDetail } from './VideoDetail'; 9 | export { default as SearchFeed } from './SearchFeed'; 10 | export { default as Videos } from './Videos'; 11 | export { default as Sidebar } from './Sidebar'; 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:16 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json to the working directory 8 | COPY package*.json ./ 9 | 10 | # Install app dependencies, including Material-UI 5 11 | RUN npm install 12 | 13 | # Copy the rest of the application code to the working directory 14 | COPY . . 15 | 16 | # Build the React app 17 | RUN npm run build 18 | 19 | # Expose the port that the app will run on (adjust if needed) 20 | EXPOSE 3000 21 | 22 | # Define the command to start the app 23 | CMD ["npm", "start"] 24 | -------------------------------------------------------------------------------- /src/components/Videos.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stack, Box } from "@mui/material"; 3 | 4 | import { ChannelCard, Loader, VideoCard } from "./"; 5 | 6 | const Videos = ({ videos, direction }) => { 7 | if(!videos?.length) return ; 8 | 9 | return ( 10 | 11 | {videos.map((item, idx) => ( 12 | 13 | {item.id.videoId && } 14 | {item.id.channelId && } 15 | 16 | ))} 17 | 18 | ); 19 | } 20 | 21 | export default Videos; 22 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import { Box } from '@mui/material'; 3 | 4 | import { ChannelDetail, VideoDetail, SearchFeed, Navbar, Feed } from './components'; 5 | 6 | const App = () => ( 7 | 8 | 9 | 10 | 11 | } /> 12 | } /> 13 | } /> 14 | } /> 15 | 16 | 17 | 18 | ); 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: my-app-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: my-app 10 | template: 11 | metadata: 12 | labels: 13 | app: my-app 14 | spec: 15 | containers: 16 | - name: my-app 17 | image: sevenajay/youtube:latest 18 | ports: 19 | - containerPort: 3000 # Your Node.js application should listen on port 3000 20 | 21 | --- 22 | 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: my-app-service 27 | spec: 28 | selector: 29 | app: my-app 30 | ports: 31 | - protocol: TCP 32 | port: 80 # You can use port 80 here for HTTP traffic 33 | targetPort: 3000 # This should match the port your Node.js application is running on (3000) 34 | type: NodePort 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | JSM Media 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/SearchFeed.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Typography, Box } from "@mui/material"; 3 | import { useParams } from "react-router-dom"; 4 | 5 | import { fetchFromAPI } from "../utils/fetchFromAPI"; 6 | import { Videos } from "./"; 7 | 8 | const SearchFeed = () => { 9 | const [videos, setVideos] = useState(null); 10 | const { searchTerm } = useParams(); 11 | 12 | useEffect(() => { 13 | fetchFromAPI(`search?part=snippet&q=${searchTerm}`) 14 | .then((data) => setVideos(data.items)) 15 | }, [searchTerm]); 16 | 17 | return ( 18 | 19 | 20 | Search Results for {searchTerm} videos 21 | 22 | 23 | 24 | {} 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default SearchFeed; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project_youtube_video_player", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.10.0", 7 | "@emotion/styled": "^11.10.0", 8 | "@mui/icons-material": "^5.8.4", 9 | "@mui/material": "^5.9.3", 10 | "axios": "^0.27.2", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-player": "^2.10.1", 14 | "react-router-dom": "^6.3.0", 15 | "react-scripts": "^5.0.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stack } from "@mui/material"; 3 | 4 | import { categories } from "../utils/constants"; 5 | 6 | const Categories = ({ selectedCategory, setSelectedCategory }) => ( 7 | 15 | {categories.map((category) => ( 16 | 32 | ))} 33 | 34 | ); 35 | 36 | export default Categories; 37 | -------------------------------------------------------------------------------- /src/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Paper, IconButton } from '@mui/material'; 4 | import SearchIcon from '@mui/icons-material/Search'; 5 | 6 | const SearchBar = () => { 7 | const [searchTerm, setSearchTerm] = useState(''); 8 | const navigate = useNavigate(); 9 | 10 | const onhandleSubmit = (e) => { 11 | e.preventDefault(); 12 | 13 | if (searchTerm) { 14 | navigate(`/search/${searchTerm}`); 15 | 16 | setSearchTerm(''); 17 | } 18 | }; 19 | 20 | return ( 21 | 32 | setSearchTerm(e.target.value)} 37 | /> 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default SearchBar; 46 | -------------------------------------------------------------------------------- /src/components/ChannelDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { Box } from "@mui/material"; 4 | 5 | import { Videos, ChannelCard } from "./"; 6 | import { fetchFromAPI } from "../utils/fetchFromAPI"; 7 | 8 | const ChannelDetail = () => { 9 | const [channelDetail, setChannelDetail] = useState(); 10 | const [videos, setVideos] = useState(null); 11 | 12 | const { id } = useParams(); 13 | 14 | useEffect(() => { 15 | const fetchResults = async () => { 16 | const data = await fetchFromAPI(`channels?part=snippet&id=${id}`); 17 | 18 | setChannelDetail(data?.items[0]); 19 | 20 | const videosData = await fetchFromAPI(`search?channelId=${id}&part=snippet%2Cid&order=date`); 21 | 22 | setVideos(videosData?.items); 23 | }; 24 | 25 | fetchResults(); 26 | }, [id]); 27 | 28 | return ( 29 | 30 | 31 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default ChannelDetail; 47 | -------------------------------------------------------------------------------- /src/components/Feed.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Box, Stack, Typography } from "@mui/material"; 3 | 4 | import { fetchFromAPI } from "../utils/fetchFromAPI"; 5 | import { Videos, Sidebar } from "./"; 6 | 7 | const Feed = () => { 8 | const [selectedCategory, setSelectedCategory] = useState("New"); 9 | const [videos, setVideos] = useState(null); 10 | 11 | useEffect(() => { 12 | setVideos(null); 13 | 14 | fetchFromAPI(`search?part=snippet&q=${selectedCategory}`) 15 | .then((data) => setVideos(data.items)) 16 | }, [selectedCategory]); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | Copyright © 2022 JSM Media 25 | 26 | 27 | 28 | 29 | 30 | {selectedCategory} videos 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default Feed; 40 | -------------------------------------------------------------------------------- /src/components/VideoCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from "react-router-dom"; 3 | import { Typography, Card, CardContent, CardMedia } from "@mui/material"; 4 | import CheckCircleIcon from "@mui/icons-material/CheckCircle"; 5 | 6 | import { demoThumbnailUrl, demoVideoUrl, demoVideoTitle, demoChannelUrl, demoChannelTitle } from "../utils/constants"; 7 | 8 | const VideoCard = ({ video: { id: { videoId }, snippet } }) => ( 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | {snippet?.title.slice(0, 60) || demoVideoTitle.slice(0, 60)} 19 | 20 | 21 | 22 | 23 | {snippet?.channelTitle || demoChannelTitle} 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | 31 | export default VideoCard -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | a { 9 | text-decoration: none; 10 | color: black; 11 | } 12 | 13 | ::-webkit-scrollbar { 14 | width: 0px; 15 | height: 5px; 16 | } 17 | 18 | ::-webkit-scrollbar-thumb { 19 | background-color: rgb(114, 113, 113); 20 | border-radius: 10px; 21 | height: 200px; 22 | } 23 | 24 | ::-webkit-scrollbar-track { 25 | background-color: transparent; 26 | } 27 | 28 | .category-btn:hover { 29 | background-color: #FC1503 !important; 30 | color: white !important; 31 | } 32 | 33 | .category-btn:hover span{ 34 | color: white !important; 35 | } 36 | 37 | .react-player { 38 | height: 77vh !important; 39 | width: 100% !important; 40 | } 41 | 42 | .search-bar{ 43 | border:none; 44 | outline:none; 45 | width:350px; 46 | } 47 | 48 | .category-btn{ 49 | font-weight: bold !important; 50 | text-transform: capitalize; 51 | display: flex; 52 | align-items: center; 53 | justify-content: start; 54 | cursor:pointer; 55 | background: transparent; 56 | outline: none; 57 | border: none; 58 | 59 | padding: 7px 15px; 60 | margin: 10px 0px; 61 | border-radius: 20px; 62 | transition: all 0.3s ease; 63 | } 64 | 65 | @media screen and (max-width: 900px) { 66 | .category-btn{ 67 | margin: 10px; 68 | } 69 | } 70 | 71 | @media screen and (max-width:800px){ 72 | .copyright{ 73 | display:none !important; 74 | } 75 | } 76 | 77 | @media screen and (max-width: 600px) { 78 | .scroll-horizontal { 79 | overflow: auto !important; 80 | } 81 | 82 | .react-player { 83 | height: 45vh !important; 84 | } 85 | .search-bar{ 86 | width:200px; 87 | } 88 | } -------------------------------------------------------------------------------- /src/components/ChannelCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, CardContent, CardMedia, Typography } from '@mui/material'; 3 | import CheckCircleIcon from '@mui/icons-material/CheckCircle'; 4 | import { Link } from 'react-router-dom'; 5 | import { demoProfilePicture } from '../utils/constants'; 6 | 7 | const ChannelCard = ({ channelDetail, marginTop }) => ( 8 | 21 | 22 | 23 | 28 | 29 | {channelDetail?.snippet?.title}{' '} 30 | 31 | 32 | {channelDetail?.statistics?.subscriberCount && ( 33 | 34 | {parseInt(channelDetail?.statistics?.subscriberCount).toLocaleString('en-US')} Subscribers 35 | 36 | )} 37 | 38 | 39 | 40 | ); 41 | 42 | export default ChannelCard; 43 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | import MusicNoteIcon from '@mui/icons-material/MusicNote'; 2 | import HomeIcon from '@mui/icons-material/Home'; 3 | import CodeIcon from '@mui/icons-material/Code'; 4 | import OndemandVideoIcon from '@mui/icons-material/OndemandVideo'; 5 | import SportsEsportsIcon from '@mui/icons-material/SportsEsports'; 6 | import LiveTvIcon from '@mui/icons-material/LiveTv'; 7 | import SchoolIcon from '@mui/icons-material/School'; 8 | import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural'; 9 | import CheckroomIcon from '@mui/icons-material/Checkroom'; 10 | import GraphicEqIcon from '@mui/icons-material/GraphicEq'; 11 | import TheaterComedyIcon from '@mui/icons-material/TheaterComedy'; 12 | import FitnessCenterIcon from '@mui/icons-material/FitnessCenter'; 13 | import DeveloperModeIcon from '@mui/icons-material/DeveloperMode'; 14 | 15 | export const logo = 'https://i.ibb.co/s9Qys2j/logo.png'; 16 | 17 | export const categories = [ 18 | { name: 'New', icon: , }, 19 | { name: 'JS Mastery', icon: , }, 20 | { name: 'Coding', icon: , }, 21 | { name: 'ReactJS', icon: , }, 22 | { name: 'NextJS', icon: , }, 23 | { name: 'Music', icon: }, 24 | { name: 'Education', icon: , }, 25 | { name: 'Podcast', icon: , }, 26 | { name: 'Movie', icon: , }, 27 | { name: 'Gaming', icon: , }, 28 | { name: 'Live', icon: , }, 29 | { name: 'Sport', icon: , }, 30 | { name: 'Fashion', icon: , }, 31 | { name: 'Beauty', icon: , }, 32 | { name: 'Comedy', icon: , }, 33 | { name: 'Gym', icon: , }, 34 | { name: 'Crypto', icon: , }, 35 | ]; 36 | 37 | export const demoThumbnailUrl = 'https://i.ibb.co/G2L2Gwp/API-Course.png'; 38 | export const demoChannelUrl = '/channel/UCmXmlB4-HJytD7wek0Uo97A'; 39 | export const demoVideoUrl = '/video/GDa8kZLNhJ4'; 40 | export const demoChannelTitle = 'JavaScript Mastery'; 41 | export const demoVideoTitle = 'Build and Deploy 5 JavaScript & React API Projects in 10 Hours - Full Course | RapidAPI'; 42 | export const demoProfilePicture = 'http://dergipark.org.tr/assets/app/images/buddy_sample.png' -------------------------------------------------------------------------------- /src/components/VideoDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link, useParams } from "react-router-dom"; 3 | import ReactPlayer from "react-player"; 4 | import { Typography, Box, Stack } from "@mui/material"; 5 | import CheckCircleIcon from "@mui/icons-material/CheckCircle"; 6 | 7 | import { Videos, Loader } from "./"; 8 | import { fetchFromAPI } from "../utils/fetchFromAPI"; 9 | 10 | const VideoDetail = () => { 11 | const [videoDetail, setVideoDetail] = useState(null); 12 | const [videos, setVideos] = useState(null); 13 | const { id } = useParams(); 14 | 15 | useEffect(() => { 16 | fetchFromAPI(`videos?part=snippet,statistics&id=${id}`) 17 | .then((data) => setVideoDetail(data.items[0])) 18 | 19 | fetchFromAPI(`search?part=snippet&relatedToVideoId=${id}&type=video`) 20 | .then((data) => setVideos(data.items)) 21 | }, [id]); 22 | 23 | if(!videoDetail?.snippet) return ; 24 | 25 | const { snippet: { title, channelId, channelTitle }, statistics: { viewCount, likeCount } } = videoDetail; 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | {title} 35 | 36 | 37 | 38 | 39 | {channelTitle} 40 | 41 | 42 | 43 | 44 | 45 | {parseInt(viewCount).toLocaleString()} views 46 | 47 | 48 | {parseInt(likeCount).toLocaleString()} likes 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default VideoDetail; 63 | --------------------------------------------------------------------------------