├── .gitignore ├── public ├── logo.png ├── play.png ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── setupTests.js ├── App.test.js ├── .gitignore ├── index.css ├── reportWebVitals.js ├── index.js ├── logo.svg └── Components │ ├── App.js │ ├── App.css │ └── Video.js ├── README.md ├── debug.log └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryBalassiano/Tik-Tok-Clone/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryBalassiano/Tik-Tok-Clone/HEAD/public/play.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryBalassiano/Tik-Tok-Clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryBalassiano/Tik-Tok-Clone/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryBalassiano/Tik-Tok-Clone/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/.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/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./Components/App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want to start measuring performance in your app, pass a function 10 | // to log results (for example: reportWebVitals(console.log)) 11 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 12 | reportWebVitals(); 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is it? 2 | 3 | A clone of the popular app TikTok, using the Reddit API it gathers videos from popular funny subreddits. 4 | 5 | ## How does it work? 6 | 7 | Using the Intersection Observer API, it checks whether the video element is visible on the screen, and plays or pauses depending if it *is* or *isn't* on the viewport. Allowing for autoplay. There's also a debounce function making it so the application isn't requesting video data with every moment a new video is displayed in the viewport. 8 | 9 | 10 | ## How do I use it? 11 | 12 | The live link to the web app is *[here](https://henrybalassiano.github.io/Tik-Tok-Clone/)* 13 | 14 | 15 | -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [1201/202535.748:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 2 | [1202/202535.753:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 3 | [1203/202535.758:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 4 | [1204/032528.349:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) 5 | [1204/032528.354:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) 6 | [1204/032528.469:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) 7 | [1204/032528.714:ERROR:exception_snapshot_win.cc(99)] thread ID 18436 not found in process 8 | [1204/032528.714:ERROR:exception_snapshot_win.cc(99)] thread ID 16724 not found in process 9 | [1204/032528.715:ERROR:exception_snapshot_win.cc(99)] thread ID 10448 not found in process 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://henrybalassiano.github.io/Tik-Tok-Clone/", 3 | "name": "tiktok-clone", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@brainhubeu/react-carousel": "^1.19.26", 8 | "@material-ui/core": "^4.11.0", 9 | "@material-ui/icons": "^4.9.1", 10 | "@testing-library/jest-dom": "^5.11.6", 11 | "@testing-library/react": "^11.2.1", 12 | "@testing-library/user-event": "^12.2.2", 13 | "gh-pages": "^3.1.0", 14 | "hls.js": "^0.14.17", 15 | "intersection-observer": "^0.12.0", 16 | "lozad": "^1.16.0", 17 | "react": "^17.0.1", 18 | "react-carousel-vertical": "^1.10.27", 19 | "react-dom": "^17.0.1", 20 | "react-intersection-observer": "^8.31.0", 21 | "react-intersection-observer-lazy-load": "^1.0.2", 22 | "react-lazyload": "^3.1.0", 23 | "react-observer-api": "^1.0.7", 24 | "react-on-screen": "^2.1.1", 25 | "react-scripts": "4.0.0", 26 | "react-virtualized": "^9.22.3", 27 | "react-window": "^1.8.6", 28 | "react-yall": "^0.1.7", 29 | "real-react-lazyload": "^1.0.2", 30 | "swiper": "^6.4.5", 31 | "vanilla-lazyload": "^17.3.0", 32 | "web-vitals": "^0.2.4" 33 | }, 34 | "scripts": { 35 | "predeploy": "npm run build", 36 | "deploy": "gh-pages -d build", 37 | "start": "react-scripts start", 38 | "build": "react-scripts build", 39 | "test": "react-scripts test", 40 | "eject": "react-scripts eject" 41 | }, 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app", 45 | "react-app/jest" 46 | ] 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 23 | 24 | 33 | FunnyTok 34 | 42 | 43 | 44 | 45 |
46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Components/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./App.css"; 3 | import Video from "./Video"; 4 | import Swiper, { Navigation } from "swiper"; 5 | import "swiper/swiper-bundle.css"; 6 | Swiper.use([Navigation]); 7 | 8 | function App() { 9 | const [loading, setload] = useState(null); 10 | const [data, setData] = useState([ 11 | { 12 | video: [], 13 | author: [], 14 | title: [], 15 | }, 16 | ]); 17 | 18 | useEffect(() => { 19 | const mySwiper = new Swiper(".swiper-container", { 20 | loop: true, 21 | spaceBetween: 830, 22 | direction: "vertical", 23 | slidesPerView: 1, 24 | speed: 100, 25 | preloadImages: true, 26 | observer: true, 27 | observeParents: true, 28 | watchSlidesVisibility: true, 29 | watchSlidesProgress: true, 30 | }); 31 | }, []); 32 | 33 | useEffect(() => { 34 | async function fetchData() { 35 | const response = await fetch( 36 | "https://www.reddit.com/r/tiktokcringe/.json?limit=100" 37 | ); 38 | 39 | const data = await response.json(); 40 | const video = []; 41 | const author = []; 42 | const title = []; 43 | const mediaData = data.data.children; 44 | try { 45 | for (var i = 7; i < mediaData.length; i++) { 46 | if ( 47 | mediaData[i].data.media !== null && 48 | mediaData[i].data.secure_media.reddit_video.bitrate_kbps < 2500 && 49 | mediaData[i].data.secure_media.reddit_video.duration < 45 50 | ) { 51 | video.push(mediaData[i].data.media); 52 | author.push(mediaData[i].data.author); 53 | title.push(mediaData[i].data.title); 54 | } 55 | } 56 | setData([ 57 | { 58 | video: video, 59 | author: author, 60 | title: title, 61 | }, 62 | ]); 63 | setload(true); 64 | } catch (err) { 65 | console.log(err); 66 | } 67 | } 68 | fetchData(); 69 | }, []); 70 | 71 | return ( 72 |
73 |
74 | {data.map(({ video, author, title }) => { 75 | return ( 76 |
77 |
78 | {video.map((v, index) => ( 79 |
80 | {loading ? ( 81 |
95 | ))} 96 |
97 |
98 | ); 99 | })} 100 |
101 |
102 | ); 103 | } 104 | 105 | export default App; 106 | -------------------------------------------------------------------------------- /src/Components/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | background-color: black; 6 | } 7 | html { 8 | overflow: hidden; 9 | } 10 | video { 11 | height: 900px; 12 | width: 450px; 13 | object-fit: cover; 14 | } 15 | 16 | audio { 17 | display: none; 18 | overflow: hidden; 19 | } 20 | 21 | .swiper-container { 22 | height: 100%; 23 | } 24 | h2 { 25 | text-align: center; 26 | z-index: 999; 27 | color: white; 28 | } 29 | #video-container { 30 | height: 100%; 31 | width: 450px; 32 | position: relative; 33 | top: 4em; 34 | display: flex; 35 | justify-content: center; 36 | margin-top: -4em; 37 | } 38 | 39 | .App { 40 | display: flex; 41 | justify-content: center; 42 | } 43 | #video-scroll { 44 | height: 845px; 45 | position: relative; 46 | top: 5em; 47 | width: 450px; 48 | overflow-x: hidden; 49 | border-radius: 30px 30px 30px 30px; 50 | } 51 | h1 { 52 | text-align: center; 53 | z-index: 999; 54 | } 55 | 56 | #play-btn { 57 | position: absolute; 58 | top: 18em; 59 | height: 100px; 60 | display: none; 61 | } 62 | #heart { 63 | position: absolute; 64 | color: white; 65 | font-size: 45px; 66 | 67 | cursor: pointer; 68 | } 69 | #heart2 { 70 | position: absolute; 71 | top: 18em; 72 | font-size: 45px; 73 | top: 16.5em; 74 | left: 9.6em; 75 | display: none; 76 | color: white; 77 | } 78 | #ld { 79 | position: absolute; 80 | left: 83%; 81 | top: 83%; 82 | z-index: 4; 83 | display: flex; 84 | flex-direction: column; 85 | } 86 | #icons { 87 | color: white; 88 | } 89 | h1 { 90 | z-index: 7; 91 | position: absolute; 92 | font-size: 0.9em; 93 | top: 90%; 94 | color: white; 95 | text-align: left; 96 | } 97 | #logo { 98 | position: absolute; 99 | height: 45px; 100 | left: 26px; 101 | top: -33px; 102 | font-size: 57px; 103 | text-shadow: 2px 2px #e70909, -3px -3px 2.4px #2bc5e0; 104 | } 105 | #loading { 106 | color: white; 107 | display: flex; 108 | justify-content: center; 109 | position: relative; 110 | top: 18em; 111 | } 112 | 113 | #title { 114 | z-index: 9999; 115 | position: relative; 116 | color: white; 117 | font-family: "Roboto", sans-serif; 118 | left: 1em; 119 | bottom: 3.7em; 120 | font-size: 0.9em; 121 | } 122 | #like-count { 123 | color: white; 124 | position: absolute; 125 | text-align: justify; 126 | bottom: -1em; 127 | left: 1.75em; 128 | } 129 | #author { 130 | text-decoration: underline; 131 | position: relative; 132 | margin: 0px; 133 | font-size: 1em; 134 | z-index: 9999; 135 | color: white; 136 | text-transform: lowercase; 137 | font-family: "Roboto", sans-serif; 138 | left: 1em; 139 | bottom: 3em; 140 | letter-spacing: 0.8px; 141 | font-weight: bolder; 142 | } 143 | #btns { 144 | display: flex; 145 | justify-content: center; 146 | } 147 | 148 | ::-webkit-scrollbar { 149 | width: 0px; /* Remove scrollbar space */ 150 | background: transparent; /* Optional: just make scrollbar invisible */ 151 | } 152 | 153 | @media only screen and (max-width: 550px) { 154 | #video-container { 155 | width: 100vw !important; 156 | border-radius: 0; 157 | display: flex; 158 | } 159 | 160 | video { 161 | border-radius: 0; 162 | width: 100%; 163 | object-fit: cover; 164 | height: 100%; 165 | } 166 | #video-scroll { 167 | width: 100vw !important; 168 | top: 0; 169 | border-radius: 0; 170 | z-index: inherit; 171 | height: 100vh; /* This is for browsers that don't support custom properties */ 172 | height: calc(var(--vh, 1vh) * 100); 173 | } 174 | #author-title { 175 | top: 78%; 176 | } 177 | #ld { 178 | top: 77%; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Components/Video.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import React, { useRef, useState, useEffect } from "react"; 3 | import IconButton from "@material-ui/core/IconButton"; 4 | import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder"; 5 | import FavoriteIcon from "@material-ui/icons/Favorite"; 6 | 7 | function useOnScreen(ref) { 8 | const [isIntersecting, setIntersecting] = useState(false); 9 | 10 | useEffect(() => { 11 | const observer = new IntersectionObserver(([entry]) => { 12 | setIntersecting(entry.isIntersecting); 13 | }); 14 | if (ref.current) { 15 | observer.observe(ref.current); 16 | } 17 | return () => { 18 | observer.unobserve(ref.current); 19 | }; 20 | }, []); 21 | 22 | return isIntersecting; 23 | } 24 | function useDebounce(value, delay) { 25 | const [debouncedValue, setDebouncedValue] = useState(value); 26 | 27 | useEffect(() => { 28 | const handler = setTimeout(() => { 29 | setDebouncedValue(value); 30 | }, delay); 31 | 32 | return () => { 33 | clearTimeout(handler); 34 | }; 35 | }, [value, delay]); 36 | 37 | return debouncedValue; 38 | } 39 | 40 | function Video({ source }) { 41 | const [click, setClicked] = useState(false); 42 | const [count, setCount] = useState(); 43 | const [canPlays, setCanPlay] = useState(false); 44 | console.log(canPlays); 45 | const videoRef = useRef(); 46 | const audioRef = useRef(false); 47 | const playRef = useRef(false); 48 | const ref = useRef(); 49 | const navSec = useRef(null); 50 | 51 | const onScreen = useOnScreen(ref); 52 | const debouncedSearchTerm = useDebounce(onScreen, 400); 53 | 54 | const clickFunc = () => { 55 | if (click === false) { 56 | setClicked(true); 57 | setCount(1); 58 | } else { 59 | setClicked(false); 60 | setCount(""); 61 | console.log(click); 62 | } 63 | }; 64 | const canPlay = (e) => { 65 | setCanPlay(true); 66 | }; 67 | 68 | useEffect(() => { 69 | if (debouncedSearchTerm === true && canPlays === true) { 70 | audioRef.current.currentTime = videoRef.current.currentTime; 71 | videoRef.current.play(); 72 | audioRef.current.play(); 73 | 74 | playRef.current.style.display = "none"; 75 | } 76 | }); 77 | 78 | const videoPlay = () => { 79 | if ( 80 | videoRef.current.paused === false && 81 | audioRef.current.paused === false 82 | ) { 83 | videoRef.current.pause(); 84 | audioRef.current.pause(); 85 | playRef.current.style.display = "block"; 86 | } else { 87 | videoRef.current.play(); 88 | audioRef.current.play(); 89 | playRef.current.style.display = "none"; 90 | } 91 | }; 92 | return ( 93 |
94 | play 95 | 96 |

F

97 | 98 |
99 | 100 | {click ? ( 101 | 102 | ) : ( 103 | 104 | )} 105 | 106 | {count} 107 |
108 | {debouncedSearchTerm ? ( 109 |
131 | ); 132 | } 133 | export default Video; 134 | --------------------------------------------------------------------------------