├── favicon.psd ├── public ├── dog1.jpg ├── dog2.jpg ├── dog3.jpg ├── dog4.jpg ├── favicon.ico ├── camera_icon.png ├── manifest.json └── index.html ├── mockups ├── steam mockup.png ├── convert mockup.png └── socialsnapper reddit mockup.png ├── prettier.config.js ├── src ├── App.test.js ├── index.js ├── components │ ├── Header.js │ ├── DownloadButton.js │ ├── Loading.js │ ├── SadMessage.js │ ├── Footer.js │ ├── Youtube │ │ ├── VideoTable.js │ │ ├── BothTable.js │ │ └── AudioTable.js │ ├── Welcome.js │ ├── MediaFetcher.js │ ├── ContactPage.js │ ├── Content.js │ ├── Twitch.js │ ├── Instagram.js │ ├── Twitter.js │ ├── Reddit.js │ ├── AboutPage.js │ └── Youtube.js ├── App.js ├── serviceWorker.js ├── popup.css └── index.css ├── .gitignore ├── README.md ├── twitter.py ├── package.json └── LICENSE /favicon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/favicon.psd -------------------------------------------------------------------------------- /public/dog1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/public/dog1.jpg -------------------------------------------------------------------------------- /public/dog2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/public/dog2.jpg -------------------------------------------------------------------------------- /public/dog3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/public/dog3.jpg -------------------------------------------------------------------------------- /public/dog4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/public/dog4.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/camera_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/public/camera_icon.png -------------------------------------------------------------------------------- /mockups/steam mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/mockups/steam mockup.png -------------------------------------------------------------------------------- /mockups/convert mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/mockups/convert mockup.png -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | arrowParens: 'always', 5 | }; 6 | -------------------------------------------------------------------------------- /mockups/socialsnapper reddit mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oftheheadland/SocialSnapper/HEAD/mockups/socialsnapper reddit mockup.png -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "react-app-polyfill/ie11"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | serviceWorker.unregister(); 10 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Header() { 4 | return ( 5 |
6 |
7 |

8 | Social 9 | Snapper 10 |

11 |
12 |
13 | ); 14 | } 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /src/components/DownloadButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function DownloadButton(props) { 4 | return ( 5 | 11 | Download 12 | 13 | ); 14 | } 15 | 16 | export default DownloadButton; 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Snapper", 3 | "name": "SocialSnapper", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.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/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FadeIn from "react-fade-in/lib/FadeIn"; 3 | 4 | function Loading() { 5 | return ( 6 | 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 | ); 17 | } 18 | 19 | export default Loading; 20 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Header from "./components/Header"; 3 | import Content from "./components/Content"; 4 | import Footer from "./components/Footer"; 5 | import "./bootstrap.min.css"; 6 | import "./index.css"; 7 | import "./popup.css"; 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/components/SadMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function SadMessage(props) { 4 | return ( 5 | <> 6 |
7 | Due to API changes at{' '} 8 | 9 | {props.url} 10 | {' '} 11 | this section of the website is no longer working. 12 |
13 | 14 |
15 | Please try out the "View Example" button below to see how this feature 16 | performed. 17 |
18 | 19 | ); 20 | } 21 | 22 | export default SadMessage; 23 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Footer() { 4 | return ( 5 | <> 6 |
7 |
8 |
9 |

10 | ©{" "} 11 | 17 | Oftheheadland 18 | 19 |

20 |
21 |
22 | 23 | ); 24 | } 25 | 26 | export default Footer; 27 | -------------------------------------------------------------------------------- /src/components/Youtube/VideoTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function VideoTable(props) { 4 | return ( 5 |
6 |

Download Video Only

7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | {props.videoRows} 20 |
QualityTypeSize 14 | 15 |
21 |
22 | ); 23 | } 24 | 25 | export default VideoTable; 26 | -------------------------------------------------------------------------------- /src/components/Youtube/BothTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function BothTable(props) { 4 | return ( 5 |
6 |

Download Video With Audio

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {props.bothRows} 19 |
QualityTypeAudio CodecSize
20 |
21 | ); 22 | } 23 | 24 | export default BothTable; 25 | -------------------------------------------------------------------------------- /src/components/Youtube/AudioTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function AudioTable(props) { 4 | return ( 5 |
6 |

Download Audio Only

7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | {props.audioRows} 20 |
Bit RateAudio CodecSize 14 | 15 |
21 |
22 | ); 23 | } 24 | 25 | export default AudioTable; 26 | -------------------------------------------------------------------------------- /src/components/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Welcome() { 4 | return ( 5 |
6 |
7 | {/*

8 | Social media websites don't make it easy to download their content. 9 |

10 |

11 | SocialSnapper allows you to save media from some of the most popular 12 | platforms. 13 |

*/} 14 |

15 | SocialSnapper lets you download images and videos from social media 16 | websites. 17 |

18 |

Select a tab and enter the URL you would like to download.

19 |
20 |
21 | ); 22 | } 23 | 24 | export default Welcome; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SocialSnapper 2 | 3 | React app for scraping download links from Social Media websites like Reddit, Instagram, and Youtube. 4 | 5 | For people who are tired of pay-walls and ads from similar websites when all they want to do is download a video of a puppy. 6 | 7 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 8 | 9 | ## Getting Started 10 | 11 | - Clone the repo 12 | - npm install 13 | - npm start 14 | 15 | Runs the app in the development mode.
16 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 17 | 18 | The page will reload if you make edits.
19 | You will also see any lint errors in the console. 20 | 21 | ## Author 22 | 23 | **Andrew VanNess** - [oftheheadland](https://github.com/oftheheadland) 24 | -------------------------------------------------------------------------------- /twitter.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import requests 3 | 4 | 5 | # multiple example 6 | # url = "https://twitter.com/Cernovich/status/1112424401872416768" 7 | 8 | # nyannyan example 9 | url = "https://twitter.com/nyannyancosplay/status/1112227719251742720" 10 | 11 | # url = "https://twitter.com/Themya47/status/1112228193543561216" 12 | 13 | # get contents from url 14 | content = requests.get(url).content 15 | 16 | # get soup 17 | soup = BeautifulSoup(content, 'lxml') # choose lxml parser 18 | 19 | # find all images 20 | image_tags = soup.findAll('img') 21 | 22 | 23 | # print out image urls 24 | for image_tag in image_tags: 25 | # print(image_tag) 26 | # try: 27 | # if 'media' in image_tag.get('src'): 28 | # print(image_tag) 29 | # print(image_tag.get('src')) 30 | # print() 31 | # except: 32 | # pass 33 | try: 34 | if 'media' in image_tag.get('src'): 35 | print(image_tag.get('src')) 36 | except: 37 | pass 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialsnapper", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@palmabit/react-cookie-law": "^0.2.4", 7 | "react": "^16.7.0", 8 | "react-app-polyfill": "^0.2.1", 9 | "react-cookie-consent": "^2.2.1", 10 | "react-dom": "^16.7.0", 11 | "react-fade-in": "^0.1.6", 12 | "react-modal": "^3.8.1", 13 | "react-popup": "^0.10.0", 14 | "react-router-dom": "^4.3.1", 15 | "react-scripts": "^2.1.5", 16 | "react-tabs": "^3.0.0" 17 | }, 18 | "devDependencies": { 19 | "husky": "1.3.1", 20 | "lint-staged": "8.1.1", 21 | "prettier": "1.16.2" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/components/MediaFetcher.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 3 | import 'react-tabs/style/react-tabs.css'; 4 | 5 | import Reddit from './Reddit'; 6 | import Instagram from './Instagram'; 7 | import Youtube from './Youtube'; 8 | // import Twitch from './Twitch'; 9 | import Twitter from './Twitter'; 10 | import Welcome from './Welcome'; 11 | 12 | class MediaFetcher extends Component { 13 | render() { 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | Reddit 21 | 22 | 23 | Instagram 24 | 25 | 26 | YouTube 27 | 28 | {/* 29 | Twitch 30 | */} 31 | 32 | Twitter 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {/* 49 | 50 | */} 51 | 52 | 53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | export default MediaFetcher; 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 33 | 37 | 38 | 44 | 45 | 46 | SocialSnapper 47 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/ContactPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FadeIn from "react-fade-in"; 3 | 4 | function ContactPage() { 5 | return ( 6 | 7 |
8 |

Contact Me

9 |
10 | 11 |

12 | 17 | Email me 18 | {" "} 19 | ideas, bugs, and feature requests. 20 |

21 |

22 | 28 | GitHub 29 | {" "} 30 | where you can see my other projects. 31 |

32 |

33 | 39 | Discord Server 40 | {" "} 41 | where you can request features and changes to the developer directly. 42 |

43 | 49 | Buy Me a Coffee at ko-fi.com 56 | 57 |
58 |
59 | ); 60 | } 61 | 62 | export default ContactPage; 63 | -------------------------------------------------------------------------------- /src/components/Content.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import FadeIn from "react-fade-in"; 3 | import MediaFetcher from "./MediaFetcher"; 4 | import AboutPage from "./AboutPage"; 5 | import ContactPage from "./ContactPage"; 6 | 7 | class Content extends Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | currentPage: "home" 12 | }; 13 | this.handleChange = this.handleChange.bind(this); 14 | } 15 | 16 | handleChange(event) { 17 | event.preventDefault(); 18 | this.setState({ currentPage: event.target.name }); 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |
25 |
26 | 36 | 37 | 47 | 48 | 58 |
59 |
60 | 61 | {this.state.currentPage === "home" ? ( 62 | 63 | 64 | 65 | ) : ( 66 | "" 67 | )} 68 | {this.state.currentPage === "about" ? : ""} 69 | {this.state.currentPage === "contact" ? : ""} 70 |
71 | ); 72 | } 73 | } 74 | export default Content; 75 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), 19 | ); 20 | 21 | export function register(config) { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Let's check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl, config); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit http://bit.ly/CRA-PWA', 45 | ); 46 | }); 47 | } else { 48 | // Is not localhost. Just register service worker 49 | registerValidSW(swUrl, config); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl, config) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then((registration) => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker == null) { 62 | return; 63 | } 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === 'installed') { 66 | if (navigator.serviceWorker.controller) { 67 | // At this point, the updated precached content has been fetched, 68 | // but the previous service worker will still serve the older 69 | // content until all client tabs are closed. 70 | console.log( 71 | 'New content is available and will be used when all ' + 72 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.', 73 | ); 74 | 75 | // Execute callback 76 | if (config && config.onUpdate) { 77 | config.onUpdate(registration); 78 | } 79 | } else { 80 | // At this point, everything has been precached. 81 | // It's the perfect time to display a 82 | // "Content is cached for offline use." message. 83 | console.log('Content is cached for offline use.'); 84 | 85 | // Execute callback 86 | if (config && config.onSuccess) { 87 | config.onSuccess(registration); 88 | } 89 | } 90 | } 91 | }; 92 | }; 93 | }) 94 | .catch((error) => { 95 | console.error('Error during service worker registration:', error); 96 | }); 97 | } 98 | 99 | function checkValidServiceWorker(swUrl, config) { 100 | // Check if the service worker can be found. If it can't reload the page. 101 | fetch(swUrl) 102 | .then((response) => { 103 | // Ensure service worker exists, and that we really are getting a JS file. 104 | const contentType = response.headers.get('content-type'); 105 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) { 106 | // No service worker found. Probably a different app. Reload the page. 107 | navigator.serviceWorker.ready.then((registration) => { 108 | registration.unregister().then(() => { 109 | window.location.reload(); 110 | }); 111 | }); 112 | } else { 113 | // Service worker found. Proceed as normal. 114 | registerValidSW(swUrl, config); 115 | } 116 | }) 117 | .catch(() => { 118 | console.log('No internet connection found. App is running in offline mode.'); 119 | }); 120 | } 121 | 122 | export function unregister() { 123 | if ('serviceWorker' in navigator) { 124 | navigator.serviceWorker.ready.then((registration) => { 125 | registration.unregister(); 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/components/Twitch.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "react-tabs/style/react-tabs.css"; 3 | import FadeIn from "react-fade-in/lib/FadeIn"; 4 | import Popup from "react-popup"; 5 | 6 | import Loading from "./Loading"; 7 | import DownloadButton from "./DownloadButton"; 8 | 9 | class Twitch extends Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | twitchClipMP4: "", // holds clip mp4 link 14 | twitchClipTitle: "", // holds clip title 15 | twitchClipFound: false, // holds clip title 16 | twitchURLinput: "", // holds value of the search input 17 | twitchLoading: false, // when true displays loading animation 18 | twitchError: false, // when true displays error message 19 | twitchDemo: true // when true, shows the "Try it out" button 20 | }; 21 | this.handleChange = this.handleChange.bind(this); 22 | this.handleTwitch = this.handleTwitch.bind(this); 23 | this.handleDemo = this.handleDemo.bind(this); 24 | this.handleReset = this.handleReset.bind(this); 25 | } 26 | 27 | handleReset(event) { 28 | event.preventDefault(); 29 | this.setState({ twitchDemo: true }); 30 | this.setState({ twitchClipFound: false }); 31 | this.setState({ twitchError: false }); 32 | document.getElementById("twitchURLInput").value = ""; 33 | this.setState({ twitchURLinput: "" }); 34 | } 35 | 36 | handleChange(event) { 37 | const { name, value } = event.target; 38 | this.setState({ [name]: value }); 39 | } 40 | 41 | handleTwitch(event) { 42 | event.preventDefault(); //prevent from reloading the page on submit 43 | 44 | if (this.state.twitchURLinput && !this.state.twitchLoading) { 45 | this.setState({ twitchDemo: false }); 46 | this.setState({ twitchLoading: true }); 47 | this.setState({ twitchError: false }); 48 | this.setState({ twitchClipFound: false }); 49 | 50 | let url = "https://snapperapi.herokuapp.com/twitchAPI"; 51 | let twitchURL = this.state.twitchURLinput; 52 | 53 | // sanitize user input; remove empty spaces 54 | let cleanTwitchURL = twitchURL.split(" ").join(""); 55 | 56 | // Build formData object. 57 | let formData = new FormData(); 58 | formData.append("url", cleanTwitchURL); 59 | 60 | const that = this; 61 | let apiFailed = false; 62 | // fetch from api 63 | fetch(url, { 64 | method: "POST", 65 | body: formData 66 | }) 67 | .then(function(response) { 68 | if (response.status !== 200) { 69 | that.setState({ twitchError: true }); 70 | that.setState({ twitchLoading: false }); 71 | apiFailed = true; 72 | } else { 73 | return response.json(); 74 | } 75 | }) 76 | .then(function(jsonData) { 77 | if (!apiFailed) { 78 | that.setState({ twitchClipMP4: jsonData["url"] }); 79 | that.setState({ twitchClipTitle: jsonData["title"] }); 80 | that.setState({ twitchClipFound: true }); 81 | that.setState({ twitchLoading: false }); 82 | } 83 | }) 84 | .catch(error => console.error("Error:", error)); 85 | } else if (!this.state.twitchLoading) { 86 | Popup.alert("Please enter a URL."); 87 | } 88 | } 89 | 90 | handleDemo(event) { 91 | event.preventDefault(); //prevent from reloading the page on submit 92 | 93 | if (!this.state.twitchLoading) { 94 | this.setState({ twitchDemo: false }); 95 | this.setState({ twitchLoading: true }); 96 | this.setState({ twitchError: false }); 97 | 98 | let url = "https://snapperapi.herokuapp.com/twitchAPI"; 99 | let twitchURL = 100 | "https://clips.twitch.tv/ObedientBenevolentBasenjiNinjaGrumpy"; 101 | document.getElementById("twitchURLInput").value = twitchURL; 102 | this.setState({ twitchURLinput: twitchURL }); 103 | 104 | // sanitize user input; remove empty spaces 105 | let cleanTwitchURL = twitchURL.split(" ").join(""); 106 | 107 | // Build formData object. 108 | let formData = new FormData(); 109 | formData.append("url", cleanTwitchURL); 110 | 111 | const that = this; 112 | let apiFailed = false; 113 | // fetch from api 114 | fetch(url, { 115 | method: "POST", 116 | body: formData 117 | }) 118 | .then(function(response) { 119 | if (response.status !== 200) { 120 | that.setState({ twitchError: true }); 121 | that.setState({ twitchLoading: false }); 122 | apiFailed = true; 123 | } else { 124 | return response.json(); 125 | } 126 | }) 127 | .then(function(jsonData) { 128 | if (!apiFailed) { 129 | that.setState({ twitchClipMP4: jsonData["url"] }); 130 | that.setState({ twitchClipTitle: jsonData["title"] }); 131 | that.setState({ twitchClipFound: true }); 132 | that.setState({ twitchLoading: false }); 133 | } 134 | }) 135 | .catch(error => console.error("Error:", error)); 136 | } 137 | } 138 | 139 | render() { 140 | const twitchContent = this.state.twitchLoading ? ( 141 | "" 142 | ) : ( 143 | 144 |
145 |
146 |

{this.state.twitchClipTitle}

147 | 151 |
152 | 153 |
154 |
155 | ); 156 | 157 | let twitchDemo = ( 158 |
159 |

160 | Your URL should look like this:{" "} 161 | 167 | https://clips.twitch.tv/ObedientBenevolentBasenjiNinjaGrumpy 168 | 169 |

170 |

171 | Here you can download Twitch.tv clips in HD quality. 172 |

173 | 176 |
177 | ); 178 | 179 | return ( 180 | 181 |
182 |
183 | 184 | 192 | 195 |
196 |
{this.state.twitchDemo ? twitchDemo : ""}
197 | 198 | 199 |
200 | {this.state.twitchError ? ( 201 |
202 | 208 | Error with your search. Please use a valid Twitch Clip URL. Your 209 | URL should look like this:{" "} 210 | 216 | https://clips.twitch.tv/ObedientBenevolentBasenjiNinjaGrumpy 217 | 218 |
219 | ) : ( 220 | "" 221 | )} 222 | {this.state.twitchLoading ? : ""} 223 |
224 | {this.state.twitchClipFound ? ( 225 | <> 226 | 232 | {twitchContent} 233 | 234 | ) : ( 235 | "" 236 | )} 237 |
238 |
239 |
240 | ); 241 | } 242 | } 243 | 244 | export default Twitch; 245 | -------------------------------------------------------------------------------- /src/components/Instagram.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import FadeIn from 'react-fade-in'; 3 | import Popup from 'react-popup'; 4 | import 'react-tabs/style/react-tabs.css'; 5 | 6 | import SadMessage from './SadMessage'; 7 | import Loading from './Loading'; 8 | 9 | class Instagram extends Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | instagramLinks: [], // holds array of instagram image and video links 14 | instagramURLinput: '', // holds value of instagram search input 15 | instagramLoading: false, // when true displays loading animation 16 | instagramError: false, // when true displays error message 17 | instagramDemo: true, // when true, shows the "Try it out" button 18 | instagramReady: false, //holds whether demo should be shown or not 19 | }; 20 | this.handleChange = this.handleChange.bind(this); 21 | this.handleInstagram = this.handleInstagram.bind(this); 22 | this.handleDemo = this.handleDemo.bind(this); 23 | this.handleReset = this.handleReset.bind(this); 24 | } 25 | 26 | handleChange(event) { 27 | const { name, value } = event.target; 28 | this.setState({ [name]: value }); 29 | } 30 | 31 | handleReset(event) { 32 | event.preventDefault(); 33 | this.setState({ instagramDemo: true }); 34 | this.setState({ instagramReady: false }); 35 | this.setState({ instagramError: false }); 36 | document.getElementById('instagramURLinput').value = ''; 37 | this.setState({ instagramURLinput: '' }); 38 | } 39 | 40 | handleDemo(event) { 41 | event.preventDefault(); 42 | 43 | if (!this.state.instagramLoading) { 44 | this.setState({ instagramDemo: false }); 45 | this.setState({ instagramReady: true }); 46 | this.setState({ instagramLoading: false }); 47 | this.setState({ instagramError: false }); 48 | this.setState({ 49 | instagramURLinput: 'https://www.instagram.com/p/Bs8qUvrhYBj/', 50 | }); 51 | this.setState({ 52 | instagramLinks: [ 53 | 'https://i.imgur.com/pSnGmAn.jpg', 54 | 'https://i.imgur.com/2M2oDbs.jpg', 55 | 'https://i.imgur.com/NLwIUu7.jpg', 56 | 'https://i.imgur.com/lTtYdbE.jpg', 57 | ], 58 | }); 59 | } 60 | } 61 | 62 | handleInstagram(event) { 63 | event.preventDefault(); //prevent from reloading the page on submit 64 | 65 | if (this.state.instagramURLinput && !this.state.instagramLoading) { 66 | this.setState({ instagramDemo: false }); 67 | this.setState({ instagramReady: false }); 68 | this.setState({ instagramLoading: true }); 69 | this.setState({ instagramError: false }); 70 | this.setState({ instagramLinks: [] }); 71 | 72 | let url = 'https://snapperapi.herokuapp.com/instagramAPI'; 73 | let instagramURL = this.state.instagramURLinput; 74 | 75 | // sanitize user input; remove empty spaces 76 | let cleanInstagramURL = instagramURL.split(' ').join(''); 77 | 78 | if (cleanInstagramURL.includes('/tv/')) { 79 | cleanInstagramURL = cleanInstagramURL.replace('/tv/', '/p/'); 80 | } 81 | 82 | // Build formData object. 83 | let formData = new FormData(); 84 | formData.append('instagramURL', cleanInstagramURL); 85 | 86 | const that = this; 87 | let apiFailed = false; 88 | // fetch from api 89 | fetch(url, { 90 | method: 'POST', 91 | body: formData, 92 | }) 93 | .then(function(response) { 94 | if (response.status !== 200) { 95 | that.setState({ instagramError: true }); 96 | that.setState({ instagramLoading: false }); 97 | apiFailed = true; 98 | } else { 99 | return response.json(); 100 | } 101 | }) 102 | .then(function(jsonData) { 103 | if (!apiFailed) { 104 | that.setState({ instagramLinks: jsonData['links'] }); 105 | that.setState({ instagramLoading: false }); 106 | that.setState({ instagramReady: true }); 107 | } 108 | }) 109 | .catch((error) => console.error('Error:', error)); 110 | } else if (!this.state.instagramLoading) { 111 | Popup.alert('Please enter a URL.'); 112 | } 113 | } 114 | 115 | render() { 116 | // IE11 compatability because it somehow still has 10% market share 117 | if (!String.prototype.includes) { 118 | // eslint-disable-next-line 119 | String.prototype.includes = function(search, start) { 120 | if (typeof start !== 'number') { 121 | start = 0; 122 | } 123 | 124 | if (start + search.length > this.length) { 125 | return false; 126 | } else { 127 | return this.indexOf(search, start) !== -1; 128 | } 129 | }; 130 | } 131 | 132 | const resetButton = ( 133 | 139 | ); 140 | 141 | const instagramBlocks = this.state.instagramLinks.map((insta, i) => ( 142 | 143 |
144 | {insta.includes('mp4') ? ( 145 |
146 | 150 |
151 | ) : ( 152 |
153 | instagram pic 154 |
155 | )} 156 | 157 |
158 | 164 | Download 165 | 166 |
167 |
168 | )); 169 | let instaDemo = ( 170 |
171 | {/*

172 | Your URL should look like this:{' '} 173 | 179 | https://www.instagram.com/p/Bs8qUvrhYBj/ 180 | 181 |

*/} 182 |

183 | Here you can download Instagram Posts, Highlights, Stories, and 184 | Profile Pictures. 185 |

186 | 189 |
190 | ); 191 | 192 | return ( 193 | 194 |
195 |
196 | 197 | {/* 205 | */} 208 | 209 |
210 | 211 |
{this.state.instagramDemo ? instaDemo : ''}
212 | 213 | 214 |
215 | {this.state.instagramReady ? <>{resetButton} : ''} 216 | 217 | {this.state.instagramError ? ( 218 | <> 219 | {resetButton} 220 |
221 | Error with your search. Please use an instagram post or story 222 | URL. 223 |
Your URL should look like this:{' '} 224 | 230 | https://www.instagram.com/p/Bs8qUvrhYBj/ 231 | 232 |
233 | 234 | ) : ( 235 | '' 236 | )} 237 | {this.state.instagramLoading ? : ''} 238 | 239 | {this.state.instagramReady ? ( 240 | <> 241 |

242 | 247 | {this.state.instagramURLinput} 248 | 249 |

250 |
{instagramBlocks}
251 | 252 | ) : ( 253 | '' 254 | )} 255 |
256 |
257 | ); 258 | } 259 | } 260 | 261 | export default Instagram; 262 | -------------------------------------------------------------------------------- /src/components/Twitter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import FadeIn from 'react-fade-in'; 3 | import Popup from 'react-popup'; 4 | import 'react-tabs/style/react-tabs.css'; 5 | import SadMessage from './SadMessage'; 6 | 7 | import Loading from './Loading'; 8 | 9 | class Twitter extends Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | twitterLinks: [], // holds array of twitter image and video links 14 | twitterURLinput: '', // holds value of twitter search input 15 | twitterLoading: false, // when true displays loading animation 16 | twitterError: false, // when true displays error message 17 | twitterDemo: true, // when true, shows the "Try it out" button 18 | twitterReady: false, //holds whether demo should be shown or not 19 | }; 20 | this.handleChange = this.handleChange.bind(this); 21 | this.handleInstagram = this.handleInstagram.bind(this); 22 | this.handleDemo = this.handleDemo.bind(this); 23 | this.handleReset = this.handleReset.bind(this); 24 | } 25 | 26 | handleChange(event) { 27 | const { name, value } = event.target; 28 | this.setState({ [name]: value }); 29 | } 30 | 31 | handleReset(event) { 32 | event.preventDefault(); 33 | this.setState({ twitterDemo: true }); 34 | this.setState({ twitterReady: false }); 35 | this.setState({ twitterError: false }); 36 | document.getElementById('twitterURLinput').value = ''; 37 | this.setState({ twitterURLinput: '' }); 38 | } 39 | 40 | handleDemo(event) { 41 | event.preventDefault(); 42 | 43 | if (!this.state.twitterLoading) { 44 | this.setState({ twitterDemo: false }); 45 | this.setState({ twitterReady: false }); 46 | this.setState({ twitterLoading: true }); 47 | this.setState({ twitterError: false }); 48 | this.setState({ twitterLinks: [] }); 49 | 50 | // https://pbs.twimg.com/media/D3WTwomV4AAwSHD?format=jpg&name=medium 51 | // https://pbs.twimg.com/media/D3WTwokUEAA2Iif?format=jpg&name=medium 52 | 53 | const demoLinks = [ 54 | 'https://pbs.twimg.com/media/D3WTwomV4AAwSHD?format=jpg&name=medium', 55 | 'https://pbs.twimg.com/media/D3WTwokUEAA2Iif?format=jpg&name=medium', 56 | ]; 57 | this.setState({ twitterLinks: demoLinks }); 58 | this.setState({ twitterLoading: false }); 59 | this.setState({ twitterReady: true }); 60 | this.setState({ 61 | twitterURLinput: 62 | 'https://twitter.com/dog_rates/status/1113958952079749121', 63 | }); 64 | } 65 | } 66 | 67 | handleInstagram(event) { 68 | event.preventDefault(); //prevent from reloading the page on submit 69 | 70 | if (this.state.twitterURLinput && !this.state.twitterLoading) { 71 | this.setState({ twitterDemo: false }); 72 | this.setState({ twitterReady: false }); 73 | this.setState({ twitterLoading: true }); 74 | this.setState({ twitterError: false }); 75 | this.setState({ twitterLinks: [] }); 76 | 77 | let url = 'https://snapperapi.herokuapp.com/twitterAPI'; 78 | let twitterURL = this.state.twitterURLinput; 79 | 80 | // sanitize user input; remove empty spaces 81 | let cleanInstagramURL = twitterURL.split(' ').join(''); 82 | 83 | // Build formData object. 84 | let formData = new FormData(); 85 | formData.append('twitterURL', cleanInstagramURL); 86 | 87 | const that = this; 88 | let apiFailed = false; 89 | // fetch from api 90 | fetch(url, { 91 | method: 'POST', 92 | body: formData, 93 | }) 94 | .then(function(response) { 95 | if (response.status !== 200) { 96 | that.setState({ twitterError: true }); 97 | that.setState({ twitterLoading: false }); 98 | apiFailed = true; 99 | } else { 100 | return response.json(); 101 | } 102 | }) 103 | .then(function(jsonData) { 104 | if (!apiFailed) { 105 | that.setState({ twitterLinks: jsonData['links'] }); 106 | that.setState({ twitterLoading: false }); 107 | that.setState({ twitterReady: true }); 108 | } 109 | }) 110 | .catch((error) => console.error('Error:', error)); 111 | } else if (!this.state.twitterLoading) { 112 | Popup.alert('Please enter a URL.'); 113 | } 114 | } 115 | 116 | render() { 117 | // IE11 compatability because it somehow still has 10% market share 118 | if (!String.prototype.includes) { 119 | // eslint-disable-next-line 120 | String.prototype.includes = function(search, start) { 121 | if (typeof start !== 'number') { 122 | start = 0; 123 | } 124 | 125 | if (start + search.length > this.length) { 126 | return false; 127 | } else { 128 | return this.indexOf(search, start) !== -1; 129 | } 130 | }; 131 | } 132 | 133 | // const twitterLinks = this.state.twitterLinks; 134 | 135 | const resetButton = ( 136 | 142 | ); 143 | 144 | const twitterBlocks = this.state.twitterLinks.map((insta, i) => ( 145 | 146 |
147 |
148 | twitter pic 149 |
150 | 151 |
152 | 158 | Download 159 | 160 |
161 |
162 | )); 163 | let instaDemo = ( 164 |
165 | {/*

166 | Your URL should look like this:{' '} 167 | 173 | https://twitter.com/dog_rates/status/1113958952079749121 174 | 175 |

*/} 176 |

177 | Here you can download all of the images from a Tweet. 178 |

179 | 182 | {/* 183 | Click here 184 | {" "} 185 | to see a demonstration. */} 186 |
187 | ); 188 | 189 | return ( 190 | 191 |
192 |
193 | 194 | {/* 203 | */} 206 | 207 |
208 | 209 |
{this.state.twitterDemo ? instaDemo : ''}
210 | 211 | 212 |
213 | {this.state.twitterReady ? <>{resetButton} : ''} 214 | 215 | {this.state.twitterError ? ( 216 | <> 217 | {resetButton} 218 |
219 | Error with your search. Please use a tweet with images in it. 220 |
Your URL should look like this:{' '} 221 | 227 | https://www.twitter.com/p/Bs8qUvrhYBj/ 228 | 229 |
230 | 231 | ) : ( 232 | '' 233 | )} 234 | {this.state.twitterLoading ? : ''} 235 | 236 | {this.state.twitterReady ? ( 237 | <> 238 |

239 | 244 | {this.state.twitterURLinput} 245 | 246 |

247 |
248 | {twitterBlocks} 249 |
250 | 251 | ) : ( 252 | '' 253 | )} 254 |
255 |
256 | ); 257 | } 258 | } 259 | 260 | export default Twitter; 261 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | /* Popup CSS https://github.com/minutemailer/react-popup/blob/gh-pages/popup.example.css*/ 2 | .mm-popup { 3 | display: none; 4 | } 5 | 6 | .mm-popup--visible { 7 | display: block; 8 | } 9 | 10 | .mm-popup__overlay { 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | width: 100%; 15 | height: 100%; 16 | z-index: 1000; 17 | overflow: auto; 18 | background: rgba(0, 0, 0, 0.1); 19 | } 20 | 21 | .mm-popup__close { 22 | position: absolute; 23 | top: 15px; 24 | right: 20px; 25 | padding: 0; 26 | width: 20px; 27 | height: 20px; 28 | cursor: pointer; 29 | outline: none; 30 | text-align: center; 31 | border-radius: 10px; 32 | border: none; 33 | text-indent: -9999px; 34 | background: transparent 35 | url("data:image/png;base64,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") 36 | no-repeat center center; 37 | background-size: 100%; 38 | margin: 0; 39 | z-index: 999; 40 | display: none; 41 | } 42 | 43 | .mm-popup__input { 44 | display: block; 45 | width: 100%; 46 | height: 30px; 47 | border-radius: 3px; 48 | background: #f5f5f5; 49 | border: 1px solid #e9ebec; 50 | outline: none; 51 | -moz-box-sizing: border-box !important; 52 | -webkit-box-sizing: border-box !important; 53 | box-sizing: border-box !important; 54 | font-size: 14px; 55 | padding: 0 12px; 56 | color: #808080; 57 | } 58 | 59 | .mm-popup__btn { 60 | /* border-radius: 3px; 61 | -moz-box-sizing: border-box; 62 | -webkit-box-sizing: border-box; 63 | box-sizing: border-box; 64 | padding: 0 10px; 65 | margin: 0; 66 | line-height: 32px; 67 | height: 32px; 68 | border: 1px solid #666; 69 | text-align: center; 70 | display: inline-block; 71 | font-size: 12px; 72 | font-weight: 400; 73 | color: #333; 74 | background: transparent; 75 | outline: none; 76 | text-decoration: none; 77 | cursor: pointer; 78 | font-family: "Open Sans", sans-serif; */ 79 | display: inline-block; 80 | font-size: 18px; 81 | cursor: pointer; 82 | text-align: center; 83 | text-decoration: none !important; 84 | outline: none; 85 | padding: 8px 25px; 86 | border: none; 87 | background-color: #de311f; 88 | color: white; 89 | border-radius: 5px; 90 | /* for animation */ 91 | overflow: hidden; 92 | position: relative; 93 | } 94 | 95 | .mm-popup__btn--success { 96 | background-color: #27ae60; 97 | border-color: #27ae60; 98 | color: #fff; 99 | } 100 | 101 | .mm-popup__btn--danger { 102 | background-color: #c5545c; 103 | border-color: #c5545c; 104 | color: #fff; 105 | } 106 | 107 | .mm-popup__box { 108 | width: 350px; 109 | position: fixed; 110 | top: 15%; 111 | left: 50%; 112 | margin-left: -175px; 113 | background: #fff; 114 | box-shadow: 0px 5px 20px 0px rgba(126, 137, 140, 0.2); 115 | /* border-radius: 5px; */ 116 | border: 1px solid #b8c8cc; 117 | overflow: hidden; 118 | z-index: 1001; 119 | } 120 | 121 | .mm-popup__box__header { 122 | padding: 15px 20px; 123 | background: #edf5f7; 124 | color: #454b4d; 125 | } 126 | 127 | .mm-popup__box__header__title { 128 | margin: 0; 129 | font-size: 16px; 130 | text-align: left; 131 | font-weight: 600; 132 | } 133 | 134 | .mm-popup__box__body { 135 | padding: 20px; 136 | line-height: 1.4; 137 | /* font-size: 14px; */ 138 | color: #454b4d; 139 | background: #fff; 140 | position: relative; 141 | z-index: 2; 142 | } 143 | 144 | .mm-popup__box__body p { 145 | margin: 0 0 5px; 146 | } 147 | 148 | .mm-popup__box__footer { 149 | overflow: hidden; 150 | padding: 10px 20px 20px; 151 | } 152 | 153 | .mm-popup__box__footer__right-space { 154 | /* float: right; */ 155 | float: none; 156 | } 157 | 158 | .mm-popup__box__footer__right-space .mm-popup__btn { 159 | margin-left: 5px; 160 | } 161 | 162 | .mm-popup__box__footer__left-space { 163 | float: left; 164 | } 165 | 166 | .mm-popup__box__footer__left-space .mm-popup__btn { 167 | margin-right: 5px; 168 | } 169 | 170 | .mm-popup__box--popover { 171 | width: 300px; 172 | margin-left: -150px; 173 | } 174 | 175 | .mm-popup__box--popover .mm-popup__close { 176 | position: absolute; 177 | top: 5px; 178 | right: 5px; 179 | padding: 0; 180 | width: 20px; 181 | height: 20px; 182 | cursor: pointer; 183 | outline: none; 184 | text-align: center; 185 | border-radius: 10px; 186 | border: none; 187 | text-indent: -9999px; 188 | background: transparent 189 | url("data:image/png;base64,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") 190 | no-repeat center center; 191 | background-size: 100%; 192 | margin: 0; 193 | z-index: 3; 194 | } 195 | 196 | .mm-popup__box--popover .mm-popup__box__body { 197 | padding: 20px; 198 | } 199 | 200 | @media (max-width: 420px) { 201 | .mm-popup__box { 202 | width: auto; 203 | left: 10px; 204 | right: 10px; 205 | top: 20%; 206 | margin-left: 0; 207 | } 208 | 209 | .mm-popup__box__footer__left-space { 210 | float: none; 211 | } 212 | 213 | .mm-popup__box__footer__right-space { 214 | float: none; 215 | } 216 | 217 | .mm-popup__box__footer { 218 | padding-top: 30px; 219 | } 220 | 221 | .mm-popup__box__footer .mm-popup__btn { 222 | display: block; 223 | width: 100%; 224 | text-align: center; 225 | margin-top: 10px; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/components/Reddit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import FadeIn from "react-fade-in"; 3 | import "react-tabs/style/react-tabs.css"; 4 | import Popup from "react-popup"; 5 | 6 | import Loading from "./Loading"; 7 | import DownloadButton from "./DownloadButton"; 8 | 9 | class Reddit extends Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | redditVideo: "", //holds reddit video url 14 | redditAudio: "", //holds reddit audio url 15 | redditURLinput: "", //holds value of reddit search bar 16 | redditReady: false, // if value is true, the reddit search results are displayed 17 | redditLoading: false, // when true the loading animation is shown 18 | encodedVideo: "", // holds value of base64 encoded reddit video url 19 | encodedAudio: "", // holds value of base64 encoded reddit audio url 20 | redditTitle: "", // holds value of Reddit post title 21 | redditThumbnail: "", // holds url of reddit post thumbnail. not currently used 22 | showOptions: "none", // show or hide audio/video only options 23 | optionsText: "More Options" 24 | }; 25 | this.handleChange = this.handleChange.bind(this); 26 | this.handleReddit = this.handleReddit.bind(this); 27 | this.handleDemo = this.handleDemo.bind(this); 28 | this.handleReset = this.handleReset.bind(this); 29 | this.handleMoreOptions = this.handleMoreOptions.bind(this); 30 | } 31 | 32 | handleMoreOptions(event) { 33 | event.preventDefault(); 34 | if (this.state.optionsText === "More Options") { 35 | this.setState({ showOptions: "block" }); 36 | this.setState({ optionsText: "Less Options" }); 37 | } else { 38 | this.setState({ showOptions: "none" }); 39 | this.setState({ optionsText: "More Options" }); 40 | } 41 | } 42 | 43 | handleChange(event) { 44 | const { name, value } = event.target; 45 | this.setState({ [name]: value }); 46 | } 47 | 48 | handleReset(event) { 49 | event.preventDefault(); 50 | this.setState({ redditReady: false }); 51 | document.getElementById("redditURLinput").value = ""; 52 | this.setState({ redditURLinput: "" }); 53 | } 54 | 55 | // handles the "Try it out" button 56 | handleDemo(event) { 57 | event.preventDefault(); 58 | if (!this.state.redditLoading) { 59 | this.setState({ redditLoading: true }); 60 | this.setState({ redditReady: true }); 61 | 62 | let url = "https://snapperapi.herokuapp.com/redditAPI"; 63 | let redditURL = 64 | "https://www.reddit.com/r/aww/comments/arz9u2/happy_baby_donkey/"; 65 | document.getElementById("redditURLinput").value = redditURL; 66 | this.setState({ redditURLinput: redditURL }); 67 | 68 | let formData = new FormData(); // Build formData object. 69 | formData.append("redditURL", redditURL); 70 | 71 | const that = this; 72 | 73 | fetch(url, { 74 | method: "POST", 75 | body: formData 76 | }) 77 | .then(function(response) { 78 | return response.json(); 79 | }) 80 | .then(function(jsonData) { 81 | that.setState({ redditVideo: jsonData["video"] }); 82 | that.setState({ redditAudio: jsonData["audio"] }); 83 | that.setState({ redditTitle: jsonData["title"] }); 84 | that.setState({ redditThumbnail: jsonData["thumbnail"] }); 85 | that.setState({ redditReady: true }); 86 | that.setState({ redditLoading: false }); 87 | that.setState({ encodedVideo: btoa(jsonData["video"]) }); 88 | that.setState({ encodedAudio: btoa(jsonData["audio"]) }); 89 | }) 90 | .catch(error => console.error("Error:", error)); 91 | } 92 | } 93 | 94 | handleReddit(event) { 95 | event.preventDefault(); //prevent from reloading the page on submit 96 | if (this.state.redditURLinput && !this.state.redditLoading) { 97 | this.setState({ redditLoading: true }); 98 | this.setState({ redditReady: true }); 99 | 100 | let url = "https://snapperapi.herokuapp.com/redditAPI"; 101 | let redditURL = this.state.redditURLinput; 102 | 103 | let formData = new FormData(); // Build formData object. 104 | formData.append("redditURL", redditURL); 105 | 106 | const that = this; 107 | 108 | fetch(url, { 109 | method: "POST", 110 | body: formData 111 | }) 112 | .then(function(response) { 113 | return response.json(); 114 | }) 115 | .then(function(jsonData) { 116 | that.setState({ redditVideo: jsonData["video"] }); 117 | that.setState({ redditAudio: jsonData["audio"] }); 118 | that.setState({ redditTitle: jsonData["title"] }); 119 | that.setState({ redditThumbnail: jsonData["thumbnail"] }); 120 | that.setState({ redditReady: true }); 121 | that.setState({ redditLoading: false }); 122 | that.setState({ encodedVideo: btoa(jsonData["video"]) }); 123 | that.setState({ encodedAudio: btoa(jsonData["audio"]) }); 124 | }) 125 | .catch(error => console.error("Error:", error)); 126 | } else if (!this.state.redditLoading) { 127 | Popup.alert("Please enter a URL."); 128 | } 129 | } 130 | 131 | render() { 132 | const displayRedditResults = this.state.redditReady; 133 | const displayRedditLoading = this.state.redditLoading; 134 | 135 | var redditDownloads = ( 136 | 137 | 140 |
141 |
142 |

{this.state.redditTitle}

143 |
144 | {/* */} 152 |