├── .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 |
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 |
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 |

95 |
96 |
F
97 |
98 |
99 |
100 | {click ? (
101 |
102 | ) : (
103 |
104 | )}
105 |
106 | {count}
107 |
108 | {debouncedSearchTerm ? (
109 |
120 | ) : (
121 |
Loading...
122 | )}
123 | {debouncedSearchTerm ? (
124 |
127 | ) : (
128 | console.log("audio element is loading")
129 | )}
130 |
131 | );
132 | }
133 | export default Video;
134 |
--------------------------------------------------------------------------------