├── .DS_Store ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── client ├── .gitignore ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── AuthRoute.js │ ├── ProtectedRoute.js │ ├── api │ └── api.js │ ├── assets │ ├── RTMP.png │ └── ohmystream.png │ ├── components │ ├── Authentication │ │ ├── FbAuth.css │ │ ├── FbAuth.js │ │ ├── TwitchAuth.css │ │ └── TwitchAuth.js │ ├── Avatars │ │ ├── BroadcastAvatar.css │ │ ├── BroadcastAvatar.js │ │ ├── DisabledBroadcastAvatar.css │ │ └── DisabledBroadcastAvatar.js │ ├── Buttons │ │ ├── BroadcastButton.css │ │ ├── BroadcastButton.js │ │ ├── Button.css │ │ ├── Button.js │ │ ├── DestinationButton.css │ │ ├── DestinationButton.js │ │ ├── ReferralButton.css │ │ ├── ReferralButton.js │ │ ├── StudioButton.css │ │ └── StudioButton.js │ ├── Canvas │ │ └── Canvas.js │ ├── Card │ │ ├── Card.css │ │ └── Card.js │ ├── ChatMessage │ │ ├── ChatMessage.css │ │ └── ChatMessage.js │ ├── Messages │ │ ├── NoDestinationsMessage.css │ │ └── NoDestinationsMessage.js │ ├── Navbar │ │ ├── Navbar.css │ │ ├── Navbar.js │ │ ├── SideNavbar.css │ │ ├── SideNavbar.js │ │ ├── SidebarData.js │ │ ├── TopNavbar.js │ │ └── TopNavbar.module.css │ ├── Selected │ │ ├── Selected.css │ │ └── Selected.js │ ├── TextArea │ │ ├── TextArea.css │ │ └── TextArea.js │ ├── TextInput │ │ ├── TextInput.css │ │ └── TextInput.js │ ├── Timer │ │ ├── Timer.css │ │ └── Timer.js │ ├── TrialExpired │ │ ├── TrialExpired.css │ │ └── TrialExpired.js │ └── ViewCounter │ │ ├── ViewCounter.css │ │ └── ViewCounter.js │ ├── constants │ └── constants.js │ ├── containers │ ├── Billing │ │ ├── Billing.css │ │ ├── Billing.js │ │ ├── PricingPlan.css │ │ └── PricingPlan.js │ ├── Broadcast │ │ ├── Broadcast.css │ │ └── Broadcast.js │ ├── Code │ │ ├── Code.css │ │ └── Code.js │ ├── Destinations │ │ ├── Destinations.css │ │ └── Destinations.js │ ├── Discount │ │ ├── Discount.css │ │ └── Discount.js │ ├── Login │ │ ├── Login.css │ │ └── Login.js │ ├── PageNotFound │ │ ├── PageNotFound.css │ │ └── PageNotFound.js │ ├── Referral │ │ ├── Referral.css │ │ └── Referral.js │ ├── Register │ │ ├── Register.css │ │ └── Register.js │ ├── Settings │ │ ├── Settings.css │ │ └── Settings.js │ └── Studio │ │ ├── Studio.css │ │ └── Studio.js │ ├── index.js │ ├── styles │ └── styles.js │ ├── utils │ ├── accurateTimer.js │ ├── deleteAllCookies.js │ ├── deleteCookie.js │ ├── eventTrack.js │ ├── formatTime.js │ ├── getCookie.js │ ├── getUrlParams.js │ ├── hubspotEmail.js │ ├── setCookie.js │ ├── shareOnFacebook.js │ ├── shareOnTwitter.js │ ├── timeFromUserRegistration.js │ ├── toastSuccessMessage.js │ ├── twitchDestinationUtils.js │ └── useInterval.js │ ├── variables.css │ └── website │ ├── Buttons │ ├── PriceButton.css │ └── PriceButton.js │ ├── Collapsible │ ├── Accordion.css │ ├── Accordion.js │ └── Panel.js │ ├── Footer │ ├── Footer.css │ └── Footer.js │ ├── Input │ ├── Input.css │ └── Input.js │ ├── PricingPlan │ ├── PricingPlan.css │ └── PricingPlan.js │ ├── Privacy │ ├── Privacy.css │ └── Privacy.js │ ├── Spinner │ ├── Spinner.js │ └── Spinner.module.css │ ├── Terms │ ├── Terms.css │ └── Terms.js │ └── Website │ ├── Website.css │ └── Website.js ├── env.example ├── env.sh ├── server ├── db.js ├── ffmpeg.js ├── ffmpeg2.js ├── ffmpeg_h.txt ├── package.json ├── routes │ ├── authentication.js │ ├── broadcasts.js │ ├── compareCode.js │ ├── destinations.js │ ├── emailEventNotifications.js │ ├── facebookAuthorization.js │ ├── facebookBroadcast.js │ ├── facebookViewCount.js │ ├── hubspot.js │ ├── referral.js │ ├── stripe.js │ ├── twitchAuthorization.js │ ├── twitchBroadcast.js │ ├── twitchViewCount.js │ ├── youtubeAuthorization.js │ ├── youtubeBroadcast.js │ └── youtubeViewCount.js ├── schema.sql ├── server.js └── utils │ ├── bindYoutubeBroadcastToStream.js │ ├── createYoutubeBroadcast.js │ ├── createYoutubeStream.js │ ├── emailCodeTemplate.js │ ├── refreshTwitchToken.js │ ├── removeDbFacebookValues.js │ ├── sendAuthCode.js │ ├── updateDbFacebookValues.js │ ├── updateDbTwitchValues.js │ ├── updateDbYoutubeValues.js │ ├── updateYoutubeAccessToken.js │ └── validateEmail.js ├── startup.sh └── update.sh /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/.DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "terminal.foreground": "#E9E9F4", 4 | "terminalCursor.background": "#E9E9F4", 5 | "terminalCursor.foreground": "#E9E9F4", 6 | "terminal.ansiBlack": "#282936", 7 | "terminal.ansiBlue": "#62D6E8", 8 | "terminal.ansiBrightBlack": "#626483", 9 | "terminal.ansiBrightBlue": "#62D6E8", 10 | "terminal.ansiBrightCyan": "#A1EFE4", 11 | "terminal.ansiBrightGreen": "#EBFF87", 12 | "terminal.ansiBrightMagenta": "#B45BCF", 13 | "terminal.ansiBrightRed": "#EA51B2", 14 | "terminal.ansiBrightWhite": "#F7F7FB", 15 | "terminal.ansiBrightYellow": "#00F769", 16 | "terminal.ansiCyan": "#A1EFE4", 17 | "terminal.ansiGreen": "#EBFF87", 18 | "terminal.ansiMagenta": "#B45BCF", 19 | "terminal.ansiRed": "#EA51B2", 20 | "terminal.ansiWhite": "#E9E9F4", 21 | "terminal.ansiYellow": "#00F769" 22 | }, 23 | "[javascript]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode", 25 | "editor.formatOnSave": true, 26 | "editor.wordWrap": "on" 27 | }, 28 | "prettier.semi": false, 29 | "prettier.singleQuote": true 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ohmystream 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livestreaming (ohmystream.co) 2 | 3 | NOTE: I am no longer actively maintaining this project. The production postgres DB is no longer running. If you want a simple example of how to use FFmpeg to restream check out [this repo](https://github.com/toshvelaga/twitch-streamer). 4 | 5 | Screen Shot 2022-03-16 at 10 56 12 PM 6 | 7 | 8 |

Ohmystream demo - Watch Video

9 | Ohmystream demo video thumbnail 10 |
11 | 12 | Check out the app [HERE](https://ohmystream.co/) 13 | 14 | Web based software to record a livestream in the browser and stream simultaneously to Youtube, Facebook, and Twitch. Software is similar in functionality to OBS and streamlabs. 15 | 16 | Frontend is built using React JS. Backend is build using Node + Express. Database is postgres. The livestreaming is enabled using websockets and ffmpeg. 17 | 18 | On the client the code that sends the livestream to the server is in the client/containers/Broadcast/Broadcast.js file. On the server the websockets are enabled in server/server.js file. 19 | 20 | ## Todo 21 | 22 | - [Trello](https://trello.com/b/W8LZ83oV/ohmystream) 23 | 24 | ## Tech/framework used 25 | 26 | Frontend tech stack: 27 | 28 | - React JS 29 | - CSS 30 | - React Router 31 | - React Icons 32 | - [react-hot-toast](https://github.com/timolins/react-hot-toast) 33 | 34 | Backend tech stack: 35 | 36 | - Node JS 37 | - Express 38 | - [node-media-server](https://github.com/illuspas/Node-Media-Server) 39 | - [ffmpeg](http://ffmpeg.org/) 40 | - [ws](https://github.com/websockets/ws) 41 | - [nodemailer](https://nodemailer.com/about/) 42 | 43 | ## Installation 44 | 45 | To run this project, install it locally using npm: 46 | 47 | ``` 48 | $ cd client 49 | $ npm install 50 | $ npm start 51 | ``` 52 | 53 | For the server create a .env file with the variables from env.example 54 | 55 | ``` 56 | $ cd server 57 | $ touch .env 58 | $ npm install 59 | $ node server.js 60 | ``` 61 | 62 | ## Contribute 63 | 64 | Feel free to use code however you would like. 65 | 66 | Feel free to also reach out to me on twitter at @t0xsh 67 | 68 | ## License 69 | 70 | MIT 71 | -------------------------------------------------------------------------------- /client/.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 | .env 16 | .vscode 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | package-lock.json 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | #gitignore 29 | .gitignore 30 | 31 | #src 32 | /src/fonts 33 | /src/assets -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livestreaming", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.7.1", 9 | "axios": "^0.21.1", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-ga": "^3.3.0", 13 | "react-ga4": "^1.0.4", 14 | "react-helmet": "^6.1.0", 15 | "react-hot-toast": "^2.1.1", 16 | "react-icons": "^4.3.1", 17 | "react-modal": "^3.14.3", 18 | "react-router": "^5.2.0", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "^5.0.0", 21 | "react-select": "^5.1.0", 22 | "react-tooltip": "^4.2.21", 23 | "socket.io-client": "^4.4.1", 24 | "web-vitals": "^1.1.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 21 | 24 | 25 | 29 | 30 | 31 | 46 | 47 | 48 | 52 | 61 | 62 | 63 | 67 | 68 | 69 | 73 | 74 | 83 | ohmystream 84 | 85 | 86 | 87 |
88 | 98 | 99 | 100 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /client/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, Suspense, lazy } from 'react' 2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' 3 | import ReactGA from 'react-ga' 4 | import Login from './containers/Login/Login' 5 | import Register from './containers/Register/Register' 6 | import Code from './containers/Code/Code' 7 | import getCookie from './utils/getCookie' 8 | import PageNotFound from './containers/PageNotFound/PageNotFound' 9 | import Broadcast from './containers/Broadcast/Broadcast' 10 | import ProtectedRoute from './ProtectedRoute' 11 | import AuthRoute from './AuthRoute' 12 | import Destinations from './containers/Destinations/Destinations' 13 | import Referral from './containers/Referral/Referral' 14 | import Studio from './containers/Studio/Studio' 15 | import Settings from './containers/Settings/Settings' 16 | import Billing from './containers/Billing/Billing' 17 | import Spinner from './website/Spinner/Spinner' 18 | import Terms from './website/Terms/Terms' 19 | import Privacy from './website/Privacy/Privacy' 20 | import Discount from './containers/Discount/Discount' 21 | import './variables.css' 22 | const Website = lazy(() => import('./website/Website/Website')) 23 | 24 | function App() { 25 | const [isLoggedIn, setisLoggedIn] = useState('') 26 | 27 | useEffect(() => { 28 | let login = getCookie('isLoggedIn') 29 | setisLoggedIn(login) 30 | }, [isLoggedIn]) 31 | 32 | useEffect(() => { 33 | ReactGA.initialize(process.env.REACT_APP_GOOGLE_ANALYTICS_ID) 34 | ReactGA.pageview(window.location.pathname + window.location.search) 35 | ReactGA.set({ 36 | username: getCookie('userEmail'), 37 | // Other relevant user information 38 | }) 39 | }, []) 40 | 41 | return ( 42 | <> 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {/* LANDING PAGE ROUTES */} 60 | 61 | 62 | }> 63 | 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | 72 | export default App 73 | -------------------------------------------------------------------------------- /client/src/AuthRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Redirect } from 'react-router-dom' 3 | import getCookie from './utils/getCookie' 4 | 5 | function AuthRoute({ component: Component, ...rest }) { 6 | const isLoggedIn = getCookie('isLoggedIn') 7 | return ( 8 | { 11 | if (!isLoggedIn) { 12 | return 13 | } else { 14 | return ( 15 | 18 | ) 19 | } 20 | }} 21 | /> 22 | ) 23 | } 24 | 25 | export default AuthRoute 26 | -------------------------------------------------------------------------------- /client/src/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, Redirect } from 'react-router-dom' 3 | import getCookie from './utils/getCookie' 4 | 5 | const ProtectedRoute = ({ component: Component, ...rest }) => { 6 | const isLoggedIn = getCookie('isLoggedIn') 7 | return ( 8 | { 11 | if (isLoggedIn) { 12 | return 13 | } else { 14 | return ( 15 | 18 | ) 19 | } 20 | }} 21 | /> 22 | ) 23 | } 24 | 25 | export default ProtectedRoute 26 | -------------------------------------------------------------------------------- /client/src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const baseURL = 4 | process.env.NODE_ENV === 'production' 5 | ? 'https://www.ohmystream.xyz/api' 6 | : 'http://localhost:5001/api' 7 | 8 | const API = axios.create({ 9 | baseURL, 10 | }) 11 | 12 | export default API 13 | -------------------------------------------------------------------------------- /client/src/assets/RTMP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/client/src/assets/RTMP.png -------------------------------------------------------------------------------- /client/src/assets/ohmystream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/client/src/assets/ohmystream.png -------------------------------------------------------------------------------- /client/src/components/Authentication/FbAuth.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/client/src/components/Authentication/FbAuth.css -------------------------------------------------------------------------------- /client/src/components/Authentication/FbAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import toastSuccessMessage from '../../utils/toastSuccessMessage' 3 | import DisabledBroadcastAvatar from '../../components/Avatars/DisabledBroadcastAvatar' 4 | import ReactTooltip from 'react-tooltip' 5 | import API from '../../api/api' 6 | import getCookie from '../../utils/getCookie' 7 | import * as FaIcons from 'react-icons/fa' 8 | 9 | /* global FB */ 10 | 11 | const FbAuth = () => { 12 | let userId = getCookie('userId') 13 | 14 | const facebookAuth = () => { 15 | FB.getLoginStatus(function (response) { 16 | console.log(response) 17 | }) 18 | FB.login( 19 | function (response) { 20 | console.log(response) 21 | console.log('FB access token:' + response.authResponse.accessToken) 22 | let facebookAccessToken = response.authResponse.accessToken 23 | let facebookUserId = response.authResponse.userID 24 | 25 | saveFacebookDataToDB(userId, facebookAccessToken, facebookUserId) 26 | facebookAuthBooleanDB() 27 | toastSuccessMessage('Facebook added as destination') 28 | }, 29 | { 30 | scope: 'email, publish_video, public_profile', 31 | auth_type: 'rerequest', 32 | } 33 | ) 34 | } 35 | 36 | const saveFacebookDataToDB = ( 37 | userId, 38 | facebookAccessToken, 39 | facebookUserId 40 | ) => { 41 | API.post('/authorize/facebook', { 42 | userId, 43 | facebookAccessToken, 44 | facebookUserId, 45 | }) 46 | } 47 | 48 | const facebookAuthBooleanDB = () => { 49 | let data = { facebookAuthBool: true, userId } 50 | API.put('/user/destinations', data) 51 | } 52 | return ( 53 | 54 | 59 | 60 | 61 | ) 62 | } 63 | 64 | export default FbAuth 65 | -------------------------------------------------------------------------------- /client/src/components/Authentication/TwitchAuth.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/client/src/components/Authentication/TwitchAuth.css -------------------------------------------------------------------------------- /client/src/components/Authentication/TwitchAuth.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { 3 | TWITCH_SCOPE, 4 | TWITCH_REDIRECT_URL_BROADCAST, 5 | } from '../../constants/constants' 6 | import { 7 | twitchAuthBooleanDB, 8 | sendCodeToTwitch, 9 | validateTwitchRequest, 10 | saveTwitchDataToDB, 11 | getTwitchStreamKey, 12 | } from '../../utils/twitchDestinationUtils' 13 | import getUrlParams from '../../utils/getUrlParams' 14 | import { useHistory } from 'react-router-dom' 15 | import getCookie from '../../utils/getCookie' 16 | import toastSuccessMessage from '../../utils/toastSuccessMessage' 17 | import DisabledBroadcastAvatar from '../../components/Avatars/DisabledBroadcastAvatar' 18 | import ReactTooltip from 'react-tooltip' 19 | import * as FaIcons from 'react-icons/fa' 20 | 21 | const TwitchAuth = () => { 22 | const twitchURL = `https://id.twitch.tv/oauth2/authorize?client_id=${process.env.REACT_APP_TWITCH_CLIENT_ID}&redirect_uri=${TWITCH_REDIRECT_URL_BROADCAST}&response_type=code&scope=${TWITCH_SCOPE}&force_verify=true` 23 | 24 | return ( 25 | (window.location.href = twitchURL)}> 26 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default TwitchAuth 37 | -------------------------------------------------------------------------------- /client/src/components/Avatars/BroadcastAvatar.css: -------------------------------------------------------------------------------- 1 | .broadcast-avatar { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 50%; 5 | border: 1px solid #ddd; 6 | margin-right: 1rem; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | box-sizing: border-box; 11 | } 12 | 13 | .broadcast-avatar:hover { 14 | cursor: pointer; 15 | background-color: #f8f8f8; 16 | /* border: 2px solid #03a9f4; */ 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/Avatars/BroadcastAvatar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './BroadcastAvatar.css' 3 | 4 | const BroadcastAvatar = (props) => { 5 | return ( 6 |
11 | {props.children} 12 |
13 | ) 14 | } 15 | 16 | export default BroadcastAvatar 17 | -------------------------------------------------------------------------------- /client/src/components/Avatars/DisabledBroadcastAvatar.css: -------------------------------------------------------------------------------- 1 | .disabled-broadcast-avatar { 2 | width: 60px; 3 | height: 60px; 4 | border-radius: 50%; 5 | border: 1px solid #ddd; 6 | margin-right: 1rem; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | box-sizing: border-box; 11 | background-color: #ddd; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/Avatars/DisabledBroadcastAvatar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './DisabledBroadcastAvatar.css' 3 | 4 | const DisabledBroadcastAvatar = (props) => { 5 | return ( 6 |
11 | {props.children} 12 |
13 | ) 14 | } 15 | 16 | export default DisabledBroadcastAvatar 17 | -------------------------------------------------------------------------------- /client/src/components/Buttons/BroadcastButton.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-font: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | 'Helvetica Neue', Ubuntu, Arial, sans-serif; 4 | } 5 | 6 | button > * { 7 | vertical-align: middle; 8 | } 9 | 10 | .broadcast-button { 11 | /* background-color: #03a9f4; */ 12 | background-color: #dddddd; 13 | color: black; 14 | letter-spacing: 2px; 15 | outline: none; 16 | border: none; 17 | padding: 12px 20px; 18 | font-size: 14px; 19 | border-radius: 5px; 20 | text-transform: uppercase; 21 | font-family: var(--main-font); 22 | margin-right: 10px; 23 | vertical-align: middle; 24 | } 25 | 26 | .broadcast-button:hover { 27 | background-color: #0298db; 28 | color: white; 29 | cursor: pointer; 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/Buttons/BroadcastButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './BroadcastButton.css' 3 | 4 | function BroadcastButton(props) { 5 | return ( 6 | <> 7 | 17 | 18 | ) 19 | } 20 | 21 | export default BroadcastButton 22 | -------------------------------------------------------------------------------- /client/src/components/Buttons/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background-color: #03a9f4; 3 | /* background-color: #3cc764; */ 4 | color: white; 5 | /* letter-spacing: 2px; */ 6 | outline: none; 7 | border: none; 8 | padding: 16px 80px; 9 | font-size: 16px; 10 | border-radius: 5px; 11 | /* text-transform: uppercase; */ 12 | display: flex; 13 | justify-content: center; 14 | font-family: var(--main-font); 15 | } 16 | 17 | @media only screen and (max-width: 600px) { 18 | .button { 19 | font-size: 16px; 20 | padding: 15px 50px; 21 | } 22 | } 23 | 24 | .button:hover { 25 | background-color: #0298db; 26 | color: white; 27 | cursor: pointer; 28 | } 29 | 30 | .loader { 31 | border: 2px solid #fff; 32 | border-radius: 50%; 33 | border-top: 2px solid transparent; 34 | /* border-bottom: 16px solid white; */ 35 | width: 1rem; 36 | height: 1rem; 37 | -webkit-animation: spin 2s linear infinite; 38 | animation: spin 2s linear infinite; 39 | margin-right: 0.5rem; 40 | } 41 | 42 | @-webkit-keyframes spin { 43 | 0% { 44 | -webkit-transform: rotate(0deg); 45 | } 46 | 100% { 47 | -webkit-transform: rotate(360deg); 48 | } 49 | } 50 | 51 | @keyframes spin { 52 | 0% { 53 | transform: rotate(0deg); 54 | } 55 | 100% { 56 | transform: rotate(360deg); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/src/components/Buttons/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './Button.css' 3 | 4 | function Button(props) { 5 | return ( 6 | <> 7 | 17 | 18 | ) 19 | } 20 | 21 | export default Button 22 | -------------------------------------------------------------------------------- /client/src/components/Buttons/DestinationButton.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-font: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | 'Helvetica Neue', Ubuntu, Arial, sans-serif; 4 | } 5 | 6 | button > * { 7 | vertical-align: middle; 8 | } 9 | 10 | .destination-button { 11 | /* background-color: #dddddd; */ 12 | background-color: var(--card-background-color); 13 | color: black; 14 | letter-spacing: 2px; 15 | outline: none; 16 | border: none; 17 | padding: 12px 16px; 18 | font-size: 14px; 19 | border-radius: 5px; 20 | text-transform: uppercase; 21 | font-family: var(--main-font); 22 | margin-right: 8px; 23 | vertical-align: middle; 24 | } 25 | 26 | .destination-button:hover { 27 | background-color: #dddddd; 28 | color: white; 29 | cursor: pointer; 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/Buttons/DestinationButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './DestinationButton.css' 3 | 4 | function DestinationButton(props) { 5 | return ( 6 | <> 7 | 17 | 18 | ) 19 | } 20 | 21 | export default DestinationButton 22 | -------------------------------------------------------------------------------- /client/src/components/Buttons/ReferralButton.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-font: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | 'Helvetica Neue', Ubuntu, Arial, sans-serif; 4 | } 5 | 6 | .referral-button { 7 | background-color: transparent; 8 | /* background-color: #f1f1f1; */ 9 | border: 1px solid #ccc; 10 | color: grey; 11 | letter-spacing: 2px; 12 | padding: 12px; 13 | outline: none; 14 | /* padding: 15px 80px; */ 15 | font-size: 16px; 16 | border-radius: 5px; 17 | text-transform: uppercase; 18 | font-family: var(--main-font); 19 | width: 100%; 20 | margin-top: 1rem; 21 | } 22 | 23 | /* Add this to align vertically */ 24 | button > img, 25 | button > span { 26 | vertical-align: middle; 27 | } 28 | 29 | .referral-button-title-span { 30 | margin-left: 10px; 31 | } 32 | 33 | @media only screen and (max-width: 600px) { 34 | .referral-button { 35 | font-size: 16px; 36 | padding: 15px 50px; 37 | } 38 | } 39 | 40 | .referral-button:hover { 41 | background-color: #f1f1f1; 42 | color: black; 43 | cursor: pointer; 44 | } 45 | -------------------------------------------------------------------------------- /client/src/components/Buttons/ReferralButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './ReferralButton.css' 3 | 4 | function ReferralButton(props) { 5 | return ( 6 | <> 7 | 15 | 16 | ) 17 | } 18 | 19 | export default ReferralButton 20 | -------------------------------------------------------------------------------- /client/src/components/Buttons/StudioButton.css: -------------------------------------------------------------------------------- 1 | button > * { 2 | vertical-align: middle; 3 | } 4 | 5 | /* .studio-button-container { 6 | margin-right: 1rem; 7 | margin-left: 1rem; 8 | } */ 9 | 10 | .studio-button { 11 | /* background-color: #03a9f4; */ 12 | background-color: var(--card-background-color); 13 | color: grey; 14 | letter-spacing: 2px; 15 | outline: none; 16 | border: none; 17 | padding: 20px 20px; 18 | font-size: 14px; 19 | border-radius: 50%; 20 | text-transform: uppercase; 21 | font-family: var(--main-font-light); 22 | /* margin-right: 30px; */ 23 | vertical-align: middle; 24 | margin-right: 1.5rem; 25 | margin-left: 1.5rem; 26 | } 27 | 28 | .studio-button:hover { 29 | /* background-color: #0298db; */ 30 | background-color: #dddddd; 31 | background-color: #2a2a2a; 32 | cursor: pointer; 33 | } 34 | 35 | .studio-button-label { 36 | color: grey; 37 | font-family: var(--main-font-light); 38 | } 39 | 40 | @media screen and (max-width: 600px) { 41 | .studio-button-label { 42 | display: none; 43 | } 44 | 45 | .studio-button { 46 | margin-right: 1rem; 47 | margin-left: 1rem; 48 | padding: 25px 25px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/src/components/Buttons/StudioButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './StudioButton.css' 3 | 4 | function StudioButton(props) { 5 | return ( 6 |
7 | 16 |

{props.label}

17 |
18 | ) 19 | } 20 | 21 | export default StudioButton 22 | -------------------------------------------------------------------------------- /client/src/components/Canvas/Canvas.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, createRef } from 'react' 2 | 3 | const Canvas = ({ videoRef, width, height }) => { 4 | const canvasRef = createRef(null) 5 | 6 | useEffect(() => { 7 | if (canvasRef.current && videoRef.current) { 8 | const interval = setInterval(() => { 9 | const ctx = canvasRef.current.getContext('2d') 10 | ctx.drawImage(videoRef.current, 0, 0, 250, 188) 11 | }, 60) 12 | return () => clearInterval(interval) 13 | } 14 | }) 15 | 16 | return 17 | } 18 | 19 | export default Canvas 20 | -------------------------------------------------------------------------------- /client/src/components/Card/Card.css: -------------------------------------------------------------------------------- 1 | .card-styles { 2 | height: 150px; 3 | min-width: 160px; 4 | border-radius: 5px; 5 | /* border: 1px solid #ccc; */ 6 | background-color: var(--card-background-color); 7 | /* transition: 0.5s; */ 8 | font-family: var(--main-font); 9 | /* margin-right: 2rem; */ 10 | } 11 | 12 | .inner-card-content { 13 | text-align: center; 14 | margin-top: 1.5rem; 15 | } 16 | 17 | .card-title { 18 | margin-top: 0; 19 | padding-top: 0; 20 | margin-bottom: 0; 21 | padding-bottom: 0; 22 | font-size: 20px; 23 | color: grey; 24 | } 25 | 26 | .remove-button { 27 | padding: 0.25rem 1rem; 28 | margin-top: 0.5rem; 29 | } 30 | 31 | .card-styles:hover { 32 | /* box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); */ 33 | box-shadow: none; 34 | border: 1px solid var(--green-color); 35 | cursor: pointer; 36 | /* background-color: #f8f8f8; */ 37 | } 38 | 39 | .card-styles:hover p { 40 | color: #fff; 41 | /* transition: 0.5s; */ 42 | } 43 | 44 | @media screen and (max-width: 600px) { 45 | .inner-card-content { 46 | /* padding-top: 1rem; */ 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/Card/Card.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import './Card.css' 3 | // import * as FaIcons from 'react-icons/fa' 4 | 5 | function Card(props) { 6 | const [displayButton, setdisplayButton] = useState(false) 7 | return ( 8 |
setdisplayButton(true)} 13 | onMouseLeave={() => setdisplayButton(false)} 14 | className='card-styles' 15 | > 16 |
17 | {props.children} 18 |

19 | {props.title} 20 |

21 | {/* DISPLAY THE REMOVE BUTTON */} 22 | {displayButton && props.selected && ( 23 | 26 | )} 27 |
28 |
29 | ) 30 | } 31 | 32 | export default React.memo(Card) 33 | -------------------------------------------------------------------------------- /client/src/components/ChatMessage/ChatMessage.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toshvelaga/livestream/eb6f05417a0db5cf4c68f8835daedd1fc61be51e/client/src/components/ChatMessage/ChatMessage.css -------------------------------------------------------------------------------- /client/src/components/ChatMessage/ChatMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './ChatMessage.css' 3 | 4 | const ChatMessage = (props) => { 5 | return
{props.message}
6 | } 7 | 8 | export default ChatMessage 9 | -------------------------------------------------------------------------------- /client/src/components/Messages/NoDestinationsMessage.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-font: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 3 | 'Helvetica Neue', Ubuntu, Arial, sans-serif; 4 | } 5 | 6 | .connect-button { 7 | /* background-color: grey; */ 8 | background: #fff; 9 | color: black; 10 | padding: 0.5em 0.75em; 11 | 12 | /* padding: 0.75rem; */ 13 | border-radius: 5px; 14 | border: 1px solid grey; 15 | cursor: pointer; 16 | font-family: var(--main-font); 17 | } 18 | 19 | .connect-button:hover { 20 | background-color: #f8f8f8; 21 | /* background-color: #dddddd; */ 22 | } 23 | -------------------------------------------------------------------------------- /client/src/components/Messages/NoDestinationsMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../../styles/styles' 3 | import { Link } from 'react-router-dom' 4 | import './NoDestinationsMessage.css' 5 | 6 | const NoDestinationsMessage = () => { 7 | return ( 8 | <> 9 |

15 | Connect YouTube, Twitch, and FB: 16 | {/* 17 | destination 18 | {' '} */} 19 | 20 | 21 | 22 |

23 | 24 | ) 25 | } 26 | 27 | export default NoDestinationsMessage 28 | -------------------------------------------------------------------------------- /client/src/components/Navbar/Navbar.css: -------------------------------------------------------------------------------- 1 | #top-navbar { 2 | overflow: hidden; 3 | background-color: #fff; 4 | position: fixed; 5 | top: 0; 6 | height: 60px; 7 | width: 100%; 8 | } 9 | 10 | #top-navbar span { 11 | float: right; 12 | display: block; 13 | color: black; 14 | text-align: center; 15 | padding: 14px 16px; 16 | text-decoration: none; 17 | font-size: 17px; 18 | } 19 | 20 | #top-navbar span:hover { 21 | background: #ddd; 22 | color: black; 23 | } 24 | 25 | .top-nav-user-button { 26 | background-color: transparent; 27 | width: 5rem; 28 | height: 2rem; 29 | border-radius: 5px; 30 | border: 1px solid #dddddd; 31 | } 32 | 33 | .top-nav-user-button:hover { 34 | /* background-color: #dddddd; */ 35 | cursor: pointer; 36 | } 37 | 38 | .notification-icon, 39 | .hamburger-icon { 40 | margin-top: 4px; 41 | } 42 | 43 | #top-navbar .hamburger-icon { 44 | float: left; 45 | margin-left: 5px; 46 | } 47 | 48 | /* mobile navbar for small screens */ 49 | 50 | .nav-menu { 51 | width: 180px; 52 | height: 100vh; 53 | display: flex; 54 | justify-content: center; 55 | position: fixed; 56 | top: 0; 57 | left: -100%; 58 | transition: 850ms; 59 | } 60 | 61 | .nav-menu.active { 62 | left: 0; 63 | transition: 350ms; 64 | } 65 | 66 | .nav-text { 67 | display: flex; 68 | justify-content: start; 69 | align-items: center; 70 | list-style: none; 71 | height: 60px; 72 | } 73 | 74 | .nav-text a { 75 | text-decoration: none; 76 | color: black; 77 | font-size: 18px; 78 | width: 95%; 79 | height: 100%; 80 | display: flex; 81 | align-items: center; 82 | padding: 0 16px; 83 | } 84 | 85 | .nav-text a:hover { 86 | background-color: #ddd; 87 | } 88 | 89 | .nav-menu-items { 90 | width: 180px; 91 | } 92 | 93 | .navbar-toggle { 94 | margin-left: 1rem; 95 | height: 60px; 96 | display: flex; 97 | justify-content: start; 98 | align-items: center; 99 | } 100 | -------------------------------------------------------------------------------- /client/src/components/Navbar/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useLayoutEffect } from 'react' 2 | import * as FaIcons from 'react-icons/fa' 3 | import { Link } from 'react-router-dom' 4 | import './Navbar.css' 5 | import SideNavbar from './SideNavbar' 6 | import TopNavbar from './TopNavbar' 7 | import { SidebarData } from './SidebarData' 8 | import { IconContext } from 'react-icons' 9 | 10 | function Navbar(props) { 11 | const [sidebar, setSidebar] = useState(false) 12 | const [width, setWidth] = useState(0) 13 | 14 | const showSidebar = () => { 15 | setSidebar(!sidebar) 16 | } 17 | 18 | useLayoutEffect(() => { 19 | const updateWindowDimensions = () => { 20 | const newWidth = window.innerWidth 21 | setWidth(newWidth) 22 | console.log('updating width') 23 | } 24 | 25 | window.addEventListener('resize', updateWindowDimensions) 26 | 27 | return () => window.removeEventListener('resize', updateWindowDimensions) 28 | }, []) 29 | 30 | const closeSideNav = () => { 31 | if (sidebar === true && width > 555) { 32 | setSidebar(false) 33 | } 34 | } 35 | 36 | return ( 37 | <> 38 | {/* sticky top navbar */} 39 | 40 | {/* {sidebar 41 | ? (document.body.style = 'background: grey') 42 | : (document.body.style = 'background: #fff')} 43 |
49 | 50 | 51 | 52 |
*/} 53 | 54 | {/* fixed side navbar with buttons */} 55 | 56 | 57 | {props.children} 58 | 59 | {/* Mobile navbar overlay for small screen size */} 60 | 61 | 80 | 81 | 82 | ) 83 | } 84 | 85 | export default Navbar 86 | -------------------------------------------------------------------------------- /client/src/components/Navbar/SideNavbar.css: -------------------------------------------------------------------------------- 1 | .side-navbar { 2 | float: left; 3 | position: fixed; 4 | top: 0; 5 | border-right: 1px solid var(--card-background-color); 6 | background-color: var(--background-color); 7 | /* background-color: #1a161e; */ 8 | width: 200px; 9 | z-index: 100; 10 | height: 100vh; 11 | padding-top: 50px; 12 | } 13 | 14 | .side-navbar-hidden { 15 | display: none; 16 | } 17 | 18 | .broadcast-li-navbar { 19 | margin-top: 100rem; 20 | } 21 | 22 | /* Style the buttons inside the tab */ 23 | .side-navbar li { 24 | display: block; 25 | background-color: inherit; 26 | padding: 14px 16px; 27 | margin: 0.4rem 0; 28 | margin-left: 8px; 29 | width: 90%; 30 | border: none; 31 | outline: none; 32 | text-align: left; 33 | cursor: pointer; 34 | transition: 0.3s; 35 | font-size: 17px; 36 | border-radius: 5px; 37 | box-sizing: border-box; 38 | } 39 | 40 | /* Change background color of buttons on hover */ 41 | .side-navbar li:hover { 42 | background-color: var(--navbar-background-color-hover); 43 | /* background-color: #212122; */ 44 | /* background-color: #272728; */ 45 | } 46 | 47 | #side-navbar-broadcasts-li { 48 | margin-top: 1.5rem; 49 | } 50 | 51 | .side-navbar-title { 52 | margin-left: 10px; 53 | font-family: var(--main-font-light); 54 | /* letter-spacing: 0.5px; */ 55 | } 56 | 57 | .side-navbar-icon, 58 | .side-navbar-title { 59 | display: inline-block; 60 | vertical-align: top; 61 | text-align: center; 62 | color: grey; 63 | } 64 | 65 | @media screen and (max-width: 1200px) { 66 | .side-navbar li { 67 | margin-left: 0; 68 | display: flex; 69 | align-self: center; 70 | width: 100%; 71 | border-radius: 0; 72 | } 73 | 74 | .side-navbar-title { 75 | display: none; 76 | } 77 | 78 | .side-navbar { 79 | width: 60px; 80 | transition: 0.3s; 81 | } 82 | 83 | .side-navbar-icon, 84 | .side-navbar-icon-active { 85 | margin-left: 5px; 86 | } 87 | } 88 | 89 | @media screen and (max-width: 592px) { 90 | .side-navbar { 91 | display: none; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/src/components/Navbar/SideNavbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useHistory } from 'react-router-dom' 3 | import * as FaIcons from 'react-icons/fa' 4 | import './SideNavbar.css' 5 | import styles from '../../styles/styles' 6 | import eventTrack from '../../utils/eventTrack' 7 | 8 | function SideNavbar(props) { 9 | const history = useHistory() 10 | const url = window.location.pathname 11 | 12 | return ( 13 |
    16 | {/* Broadcasts */} 17 |
  • history.push('/broadcast')} 20 | style={ 21 | window.location.pathname === '/broadcast' 22 | ? { backgroundColor: styles.sideNavbarHoverColor } 23 | : null 24 | } 25 | > 26 |
    32 | 33 |
    34 |
    40 | Broadcast 41 |
    42 |
  • 43 | {/* Destinations */} 44 |
  • { 46 | history.push('/destinations') 47 | eventTrack('App', 'Destinations Tab Clicked', 'Button') 48 | }} 49 | style={ 50 | window.location.pathname === '/destinations' 51 | ? { backgroundColor: styles.sideNavbarHoverColor } 52 | : null 53 | } 54 | > 55 |
    63 | 64 |
    65 |
    73 | Destinations 74 |
    75 |
  • 76 | {/* Referrals */} 77 | 78 | {/*
  • history.push('/referrals')} 80 | style={ 81 | window.location.pathname === '/referrals' 82 | ? { backgroundColor: styles.sideNavbarHoverColor } 83 | : null 84 | } 85 | className='tablinks' 86 | > 87 |
    88 | 89 |
    90 |
    Referrals
    91 |
  • */} 92 |
93 | ) 94 | } 95 | 96 | export default SideNavbar 97 | -------------------------------------------------------------------------------- /client/src/components/Navbar/SidebarData.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as FaIcons from 'react-icons/fa' 3 | 4 | export const SidebarData = [ 5 | { 6 | title: 'Broadcast', 7 | path: '/broadcast', 8 | icon: , 9 | cName: 'nav-text', 10 | }, 11 | { 12 | title: 'Destinations', 13 | path: '/destinations', 14 | icon: , 15 | cName: 'nav-text', 16 | }, 17 | ] 18 | -------------------------------------------------------------------------------- /client/src/components/Navbar/TopNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react' 2 | import * as FaIcons from 'react-icons/fa' 3 | import navbarStyles from './TopNavbar.module.css' 4 | import deleteAllCookies from '../../utils/deleteAllCookies' 5 | import { Link, useHistory, useLocation, history } from 'react-router-dom' 6 | import styles from '../../styles/styles' 7 | 8 | function TopNavbar(props) { 9 | const history = useHistory() 10 | 11 | const handleClick = () => { 12 | history.push('/billing') 13 | } 14 | const url = window.location.pathname 15 | 16 | return ( 17 | 18 | {/* } /> */} 19 | {props.children} 20 | {!url.includes('/studio') && ( 21 | <> 22 | 25 | 31 | } 32 | > 33 | 34 | 35 | 36 | )} 37 | 38 | ) 39 | } 40 | 41 | function Navbar(props) { 42 | return ( 43 | 46 | ) 47 | } 48 | 49 | function NavItem(props) { 50 | const [open, setOpen] = useState(false) 51 | const naviconRef = useRef(null) 52 | useOutsideAlerter(naviconRef) 53 | 54 | function useOutsideAlerter(ref) { 55 | useEffect(() => { 56 | /** 57 | * close the topnavbar dropdown if it is open 58 | */ 59 | function handleClickOutside(event) { 60 | if (ref.current && !ref.current.contains(event.target) && open) { 61 | setOpen(false) 62 | } 63 | } 64 | 65 | // Bind the event listener 66 | document.addEventListener('mousedown', handleClickOutside) 67 | return () => { 68 | // Unbind the event listener on clean up 69 | document.removeEventListener('mousedown', handleClickOutside) 70 | } 71 | }, [ref, open]) 72 | } 73 | 74 | const closeNavbar = () => { 75 | setOpen(!open) 76 | } 77 | 78 | return ( 79 |
  • 80 | 81 | {props.icon} 82 | 83 | 84 | {open && props.children} 85 |
  • 86 | ) 87 | } 88 | 89 | function DropdownMenu() { 90 | const [activeMenu, setActiveMenu] = useState('main') 91 | const dropdownRef = useRef(null) 92 | 93 | const history = useHistory() 94 | 95 | function DropdownItem(props) { 96 | return ( 97 | 98 | {props.children} 99 | 100 | ) 101 | } 102 | 103 | const onLogout = () => { 104 | deleteAllCookies() 105 | history.push('/login') 106 | } 107 | 108 | return ( 109 |
    110 |
    111 | history.push('/broadcast')}> 112 | Broadcast 113 | 114 | history.push('/destinations')}> 115 | Destinations 116 | 117 | history.push('/billing')}> 118 | Billing 119 | 120 | history.push('/settings')}> 121 | Settings 122 | 123 | Logout 124 |
    125 |
    126 | ) 127 | } 128 | 129 | export default TopNavbar 130 | -------------------------------------------------------------------------------- /client/src/components/Navbar/TopNavbar.module.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | :root { 6 | /* --bg: #2c2b30; */ 7 | --bg: var(--main-background-color); 8 | --bg-accent: #f1f1f1; 9 | --nav-size: 60px; 10 | --border: 1px solid var(--card-background-color); 11 | --border-radius: 8px; 12 | --speed: 500ms; 13 | } 14 | 15 | ul { 16 | list-style: none; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | a { 22 | color: var(--text-color); 23 | text-decoration: none; 24 | font-family: var(--main-font); 25 | } 26 | 27 | /* Top Navigation Bar */ 28 | 29 | /*